API 设计指南
为了方便快速参考,许多指南的细节可以单独展开。当打印此页面时,细节永远不会被隐藏。
目录
引言
在编写 Swift 代码时,提供清晰、一致的开发者体验,很大程度上取决于 API 中出现的名称和惯用法。这些设计指南解释了如何确保你的代码感觉像是更大的 Swift 生态系统的一部分。
基本原则
-
使用时的清晰性是你最重要的目标。诸如方法和属性之类的实体只声明一次,但会重复使用。设计 API 以使这些使用清晰简洁。在评估设计时,阅读声明很少足够;始终检查用例以确保它在上下文中看起来清晰。
-
清晰性比简洁性更重要。 虽然 Swift 代码可以很紧凑,但启用字符最少的尽可能小的代码并非目标。Swift 代码的简洁性(如果出现)是强大的类型系统和自然减少样板代码的功能的副作用。
-
为每个声明编写文档注释。编写文档获得的见解可能会对你的设计产生深远的影响,因此不要拖延。
如果你在用简单的术语描述 API 的功能时遇到困难,你可能设计了错误的 API。
-
使用 Swift 的 Markdown 方言。
-
从摘要开始,描述正在声明的实体。通常,API 可以通过其声明和摘要完全理解。
/// **Returns a "view" of `self` containing the same elements in** /// **reverse order.** func reversed() -> ReverseCollection<Self>
-
专注于摘要;它是最重要的部分。许多优秀的文档注释仅包含一个出色的摘要。
-
尽可能使用单个句子片段,以句点结尾。不要使用完整的句子。
-
描述函数或方法做什么以及它返回什么,省略空效果和
Void
返回值/// **Inserts** `newHead` at the beginning of `self`. mutating func prepend(_ newHead: Int) /// **Returns** a `List` containing `head` followed by the elements /// of `self`. func prepending(_ head: Element) -> List /// **Removes and returns** the first element of `self` if non-empty; /// returns `nil` otherwise. mutating func popFirst() -> Element?
注意:在像上面的
popFirst
这样的罕见情况下,摘要由分号分隔的多个句子片段组成。 -
描述下标访问什么:
/// **Accesses** the `index`th element. subscript(index: Int) -> Element { get set }
-
描述初始化器创建什么:
/// **Creates** an instance containing `n` repetitions of `x`. init(count n: Int, repeatedElement x: Element)
-
对于所有其他声明,描述声明的实体是什么。
/// **A collection that** supports equally efficient insertion/removal /// at any position. struct List { /// **The element at the beginning** of `self`, or `nil` if self is /// empty. var first: Element? ...
-
-
可选地,继续使用一个或多个段落和项目符号。段落之间用空行分隔,并使用完整的句子。
/// Writes the textual representation of each <span class="graphic">←</span><span class="commentary"> Summary</span> /// element of `items` to the standard output. /// <span class="graphic">←</span><span class="commentary"> Blank line</span> /// The textual representation for each item `x` <span class="graphic">←</span><span class="commentary"> Additional discussion</span> /// is generated by the expression `String(x)`. /// /// - **Parameter separator**: text to be printed <span class="graphic">⎫</span> /// between items. <span class="graphic">⎟</span> /// - **Parameter terminator**: text to be printed <span class="graphic">⎬</span><span class="commentary"> <a href="https://developer.apple.com/library/prerelease/mac/documentation/Xcode/Reference/xcode_markup_formatting_ref/SymbolDocumentation.html#//apple_ref/doc/uid/TP40016497-CH51-SW14">Parameters section</a></span> /// at the end. <span class="graphic">⎟</span> /// <span class="graphic">⎭</span> /// - **Note**: To print without a trailing <span class="graphic">⎫</span> /// newline, pass `terminator: ""` <span class="graphic">⎟</span> /// <span class="graphic">⎬</span><span class="commentary"> <a href="https://developer.apple.com/library/prerelease/mac/documentation/Xcode/Reference/xcode_markup_formatting_ref/SymbolDocumentation.html#//apple_ref/doc/uid/TP40016497-CH51-SW13">Symbol commands</a></span> /// - **SeeAlso**: `CustomDebugStringConvertible`, <span class="graphic">⎟</span> /// `CustomStringConvertible`, `debugPrint`. <span class="graphic">⎭</span> public func print<Target: OutputStreamType>( _ items: Any..., separator: String = " ", terminator: String = "\n")
-
命名
促进清晰的使用
-
包含避免歧义所需的所有单词,以便阅读代码的人理解名称的含义。
例如,考虑一个方法,该方法删除集合中给定位置的元素。
extension List { public mutating func remove(at position: Index) -> Element } employees.remove(at: x)
如果我们从方法签名中省略单词
at
,则可能会向读者暗示该方法搜索并删除与x
相等的元素,而不是使用x
来指示要删除的元素的位置。employees.remove(x) // unclear: are we removing x?
-
省略不必要的单词。 名称中的每个单词都应在使用位置传达重要的信息。
可能需要更多单词来阐明意图或消除歧义,但是应该省略与读者已经拥有的信息冗余的单词。特别是,省略仅仅重复类型信息的单词。
public mutating func removeElement(_ member: Element) -> Element? allViews.removeElement(cancelButton)
在这种情况下,单词
Element
在调用位置没有添加任何重要的信息。这个 API 最好是public mutating func remove(_ member: Element) -> Element? allViews.remove(cancelButton) // clearer
有时,重复类型信息对于避免歧义是必要的,但通常最好使用描述参数角色而不是其类型的单词。有关详细信息,请参见下一项。
-
根据变量、参数和关联类型的角色命名它们, 而不是它们的类型约束。
var **string** = "Hello" protocol ViewController { associatedtype **View**Type : View } class ProductionLine { func restock(from **widgetFactory**: WidgetFactory) }
以这种方式重新使用类型名称无法优化清晰度和表达性。相反,应努力选择表达实体角色的名称。
var **greeting** = "Hello" protocol ViewController { associatedtype **ContentView** : View } class ProductionLine { func restock(from **supplier**: WidgetFactory) }
如果关联类型与其协议约束紧密绑定,以至于协议名称就是角色,请通过在协议名称后附加
Protocol
来避免冲突protocol Sequence { associatedtype Iterator : Iterator**Protocol** } protocol Iterator**Protocol** { ... }
-
补偿弱类型信息以阐明参数的角色。
特别是当参数类型是
NSObject
、Any
、AnyObject
或基本类型(如Int
或String
)时,使用点的类型信息和上下文可能无法完全传达意图。在此示例中,声明可能很清楚,但是使用位置含糊不清。func add(_ observer: NSObject, for keyPath: String) grid.add(self, for: graphics) // vague
为了恢复清晰性,在每个弱类型参数前面加上一个名词来描述其角色
func add**Observer**(_ observer: NSObject, for**KeyPath** path: String) grid.addObserver(self, forKeyPath: graphics) // clear
力求流畅的使用
-
首选使用方法和函数名称,使使用位置形成符合语法规则的英语短语。
x.insert(y, at: z) <span class="commentary">“x, insert y at z”</span> x.subviews(havingColor: y) <span class="commentary">“x's subviews having color y”</span> x.capitalizingNouns() <span class="commentary">“x, capitalizing nouns”</span>
x.insert(y, position: z) x.subviews(color: y) x.nounCapitalize()
当第一个或前两个参数对于调用的含义不是核心时,流畅性有所降低是可以接受的
AudioUnit.instantiate( with: description, **options: [.inProcess], completionHandler: stopProgressBar**)
-
工厂方法的名称以“
make
”开头, 例如x.makeIterator()
。 -
初始化器和工厂方法调用的第一个参数不应构成以基本名称开头的短语,例如
x.makeWidget(cogCount: 47)
例如,以下调用中的第一个参数不能与基本名称的同一短语一起阅读
let foreground = **Color**(red: 32, green: 64, blue: 128) let newPart = **factory.makeWidget**(gears: 42, spindles: 14) let ref = **Link**(target: destination)
在下面,API 作者试图创建与第一个参数的语法连续性。
let foreground = **Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)** let newPart = **factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)** let ref = **Link(to: destination)**
实际上,此指南以及关于实参标签的指南意味着,除非调用执行值保留类型转换,否则第一个参数将带有标签。
let rgbForeground = RGBColor(cmykForeground)
-
根据函数和方法的副作用命名它们
-
没有副作用的函数和方法应读作名词短语,例如
x.distance(to: y)
、i.successor()
。 -
具有副作用的函数和方法应读作祈使动词短语,例如
print(x)
、x.sort()
、x.append(y)
。 -
一致地命名可变/不可变方法对。可变方法通常具有语义相似的不可变变体,但是该变体返回一个新值而不是就地更新实例。
-
当操作自然地用动词描述时,对可变方法使用动词的祈使形式,并应用“ed”或“ing”后缀来命名其不可变对应项。
可变 不可变 x.sort()
z = x.sorted()
x.append(y)
z = x.appending(y)
-
首选使用动词的过去分词(通常附加“ed”)来命名不可变变体
/// Reverses `self` in-place. mutating func reverse() /// Returns a reversed copy of `self`. func revers**ed**() -> Self ... x.reverse() let y = x.reversed()
-
当添加“ed”在语法上不正确(因为动词带有直接宾语)时,通过附加“ing”使用动词的现在分词来命名不可变变体。
/// Strips all the newlines from `self` mutating func stripNewlines() /// Returns a copy of `self` with all the newlines stripped. func strip**ping**Newlines() -> String ... s.stripNewlines() let oneLine = t.strippingNewlines()
-
-
当操作自然地用名词描述时,对不可变方法使用名词,并应用“form”前缀来命名其可变对应项。
不可变 可变 x = y.union(z)
y.formUnion(z)
j = c.successor(i)
c.formSuccessor(&i)
-
-
-
布尔方法和属性的使用应读作关于接收者的断言,当使用是不可变的时,例如
x.isEmpty
、line1.intersects(line2)
。 -
描述事物是什么的协议应读作名词(例如
Collection
)。 -
描述能力的协议应使用后缀
able
、ible
或ing
命名(例如Equatable
、ProgressReporting
)。 -
其他类型、属性、变量和常量的名称应读作名词。
恰当使用术语
- 术语
- 名词 - 在特定领域或行业内具有精确的、专门含义的单词或短语。
-
如果更常见的词可以同样很好地传达含义,则避免使用晦涩的术语。如果“皮肤”可以满足你的目的,请不要说“表皮”。术语是重要的沟通工具,但应仅用于捕获否则会丢失的关键含义。
-
如果确实使用术语,请坚持已建立的含义。
使用技术术语而不是更常见的词的唯一原因是它可以精确地表达否则会模棱两可或不清楚的内容。因此,API 应严格按照其公认的含义使用该术语。
-
不要让专家感到惊讶:任何已经熟悉该术语的人,如果我们似乎为其发明了新的含义,都会感到惊讶,甚至可能会生气。
-
不要使初学者感到困惑:任何试图学习该术语的人都可能会进行网络搜索并找到其传统含义。
-
-
避免缩写。 缩写,尤其是非标准缩写,实际上是术语,因为理解取决于将它们正确地翻译成非缩写形式。
你应该使用的任何缩写的预期含义都应该很容易通过网络搜索找到。
-
接受先例。 不要为了完全的初学者而优化术语,以牺牲与现有文化的符合性为代价。
最好将连续的数据结构命名为
Array
,而不是使用简化的术语(例如List
),即使初学者可能更容易掌握List
的含义。数组是现代计算的基础,因此每个程序员都知道(或很快会知道)数组是什么。使用大多数程序员熟悉的术语,他们的网络搜索和问题将得到回报。在特定的编程领域(例如数学)中,广泛先例的术语(例如
sin(x)
)优于解释性短语(例如verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
)。请注意,在这种情况下,先例胜过避免缩写的指南:尽管完整单词是sine
,但 “sin(x)” 在程序员中使用了数十年,在数学家中使用了数百年。
约定
通用约定
-
记录任何非 O(1) 计算属性的复杂度。 人们经常假设属性访问不涉及重要的计算,因为他们以存储的属性作为心理模型。当这种假设可能被违反时,请务必提醒他们。
-
方法和属性优先于自由函数。 自由函数仅在特殊情况下使用
-
当没有明显的
self
时min(x, y, z)
-
当函数是无约束的泛型时
print(x)
-
当函数语法是已建立的领域符号的一部分时
sin(x)
-
-
遵循大小写约定。 类型和协议的名称为
UpperCamelCase
。其他所有名称均为lowerCamelCase
。在美国英语中通常全部以大写字母出现的首字母缩略词和缩写词应根据大小写约定统一大写或小写
var **utf8**Bytes: [**UTF8**.CodeUnit] var isRepresentableAs**ASCII** = true var user**SMTP**Server: Secure**SMTP**Server
其他首字母缩略词应视为普通单词
var **radar**Detector: **Radar**Scanner var enjoys**Scuba**Diving = true
-
例如,建议使用以下方法,因为这些方法本质上做的是相同的事情
extension Shape { /// Returns `true` if `other` is within the area of `self`; /// otherwise, `false`. func **contains**(_ other: **Point**) -> Bool { ... } /// Returns `true` if `other` is entirely within the area of `self`; /// otherwise, `false`. func **contains**(_ other: **Shape**) -> Bool { ... } /// Returns `true` if `other` is within the area of `self`; /// otherwise, `false`. func **contains**(_ other: **LineSegment**) -> Bool { ... } }
而且由于几何类型和集合是不同的领域,因此在同一个程序中使用也是可以的
extension Collection where Element : Equatable { /// Returns `true` if `self` contains an element equal to /// `sought`; otherwise, `false`. func **contains**(_ sought: Element) -> Bool { ... } }
但是,这些
index
方法具有不同的语义,应该使用不同的名称extension Database { /// Rebuilds the database's search index func **index**() { ... } /// Returns the `n`th row in the given table. func **index**(_ n: Int, inTable: TableID) -> TableRow { ... } }
最后,避免“重载返回类型”,因为它会在类型推断存在时导致歧义。
extension Box { /// Returns the `Int` stored in `self`, if any, and /// `nil` otherwise. func **value**() -> Int? { ... } /// Returns the `String` stored in `self`, if any, and /// `nil` otherwise. func **value**() -> String? { ... } }
参数
func move(from **start**: Point, to **end**: Point)
-
选择参数名称以服务于文档。即使参数名称不会出现在函数或方法的使用点,它们也起着重要的解释作用。
选择这些名称,使文档易于阅读。例如,这些名称使文档阅读起来很自然
/// Return an `Array` containing the elements of `self` /// that satisfy `**predicate**`. func filter(_ **predicate**: (Element) -> Bool) -> [Generator.Element] /// Replace the given `**subRange**` of elements with `**newElements**`. mutating func replaceRange(_ **subRange**: Range<Index>, with **newElements**: [E])
然而,这些名称使文档显得笨拙且不合语法
/// Return an `Array` containing the elements of `self` /// that satisfy `**includedInResult**`. func filter(_ **includedInResult**: (Element) -> Bool) -> [Generator.Element] /// Replace the **range of elements indicated by `r`** with /// the contents of `**with**`. mutating func replaceRange(_ **r**: Range<Index>, **with**: [E])
-
当默认参数可以简化常用用法时,请加以利用。任何具有单个常用值的参数都是默认参数的候选者。
默认参数通过隐藏不相关的信息来提高可读性。例如
let order = lastName.compare( royalFamilyName**, options: [], range: nil, locale: nil**)
可以变成更简单的
let order = lastName.**compare(royalFamilyName)**
默认参数通常比使用方法族更可取,因为它们降低了任何试图理解 API 的人的认知负担。
extension String { /// *...description...* public func compare( _ other: String, options: CompareOptions **= []**, range: Range<Index>? **= nil**, locale: Locale? **= nil** ) -> Ordering }
以上可能并不简单,但它比以下情况简单得多
extension String { /// *...description 1...* public func **compare**(_ other: String) -> Ordering /// *...description 2...* public func **compare**(_ other: String, options: CompareOptions) -> Ordering /// *...description 3...* public func **compare**( _ other: String, options: CompareOptions, range: Range<Index>) -> Ordering /// *...description 4...* public func **compare**( _ other: String, options: StringCompareOptions, range: Range<Index>, locale: Locale) -> Ordering }
方法族中的每个成员都需要单独记录并由用户理解。为了在它们之间做出决定,用户需要理解所有这些方法,并且偶尔会出现令人惊讶的关系——例如,
foo(bar: nil)
和foo()
并不总是同义词——这使得在几乎相同的文档中找出细微差别成为一个乏味的过程。使用带有默认参数的单个方法提供了 гораздо 更好的程序员体验。 -
优先将带有默认值的参数放在参数列表的末尾。没有默认值的参数通常对于方法的语义更为重要,并提供了方法被调用的稳定初始使用模式。
-
如果您的 API 将在生产环境中运行,请优先使用
#fileID
而不是其他替代方案。#fileID
可以节省空间并保护开发人员的隐私。如果完整路径将简化开发工作流程或用于文件 I/O,请在最终用户永远不会运行的 API(例如测试助手和脚本)中使用#filePath
。使用#file
以保持与 Swift 5.2 或更早版本的源代码兼容性。
实参标签
func move(**from** start: Point, **to** end: Point)
x.move(**from:** x, **to:** y)
-
当实参无法有效区分时,省略所有标签,例如
min(number1, number2)
、zip(sequence1, sequence2)
。 -
在执行值保留类型转换的初始化器中,省略第一个实参标签,例如
Int64(someUInt32)
第一个实参应始终是转换的源。
extension String { // Convert `x` into its textual representation in the given radix init(**_** x: BigInt, radix: Int = 10) <span class="commentary">← Note the initial underscore</span> } text = "The value is: " text += **String(veryLargeNumber)** text += " and in hexadecimal, it's" text += **String(veryLargeNumber, radix: 16)**
但是,在“窄化”类型转换中,建议使用描述窄化的标签。
extension UInt32 { /// Creates an instance having the specified `value`. init(**_** value: Int16) <span class="commentary">← Widening, so no label</span> /// Creates an instance having the lowest 32 bits of `source`. init(**truncating** source: UInt64) /// Creates an instance having the nearest representable /// approximation of `valueToApproximate`. init(**saturating** valueToApproximate: UInt64) }
值保留类型转换是一种 单态性,即,源值中的每个差异都会导致结果值中的差异。例如,从
Int8
到Int64
的转换是值保留的,因为每个不同的Int8
值都会转换为不同的Int64
值。但是,反方向的转换可能无法保留值:Int64
具有比Int8
中可以表示的更多可能值。注意:检索原始值的能力与转换是否为值保留无关。
-
当第一个实参构成 介词短语 的一部分时,为其指定一个实参标签。实参标签通常应从 介词 开始,例如
x.removeBoxes(havingLength: 12)
。当最初两个实参表示单个抽象的各个部分时,会出现例外情况。
a.move(**toX:** b, **y:** c) a.fade(**fromRed:** b, **green:** c, **blue:** d)
在这种情况下,在介词之后开始实参标签,以保持抽象清晰。
a.moveTo(**x:** b, **y:** c) a.fadeFrom(**red:** b, **green:** c, **blue:** d)
-
否则,如果第一个实参构成语法短语的一部分,则省略其标签,并将任何前面的单词附加到基本名称,例如
x.addSubview(y)
此指南意味着,如果第一个实参不构成语法短语的一部分,则应具有标签。
view.dismiss(**animated:** false) let text = words.split(**maxSplits:** 12) let studentsByName = students.sorted(**isOrderedBefore:** Student.namePrecedes)
请注意,短语传达正确的含义很重要。以下内容在语法上是正确的,但会表达错误的内容。
view.dismiss(false) <span class="commentary">Don't dismiss? Dismiss a Bool?</span> words.split(12) <span class="commentary">Split the number 12?</span>
另请注意,带有默认值的实参可以省略,在这种情况下,它们不构成语法短语的一部分,因此它们应始终具有标签。
-
标记所有其他实参.
特殊说明
-
在 API 中出现元组成员和命名闭包参数时,请标记它们。
这些名称具有解释力,可以从文档注释中引用,并提供对元组成员的富有表现力的访问。
/// Ensure that we hold uniquely-referenced storage for at least /// `requestedCapacity` elements. /// /// If more storage is needed, `allocate` is called with /// **`byteCount`** equal to the number of maximally-aligned /// bytes to allocate. /// /// - Returns: /// - **reallocated**: `true` if a new block of memory /// was allocated; otherwise, `false`. /// - **capacityChanged**: `true` if `capacity` was updated; /// otherwise, `false`. mutating func ensureUniqueStorage( minimumCapacity requestedCapacity: Int, allocate: (_ **byteCount**: Int) -> UnsafePointer<Void> ) -> (**reallocated:** Bool, **capacityChanged:** Bool)
用于闭包参数的名称应像顶级函数的参数名称一样选择。不支持在调用点出现的闭包实参的标签。
-
对于不受约束的多态性(例如
Any
、AnyObject
和不受约束的泛型参数),请格外小心,以避免重载集中的歧义。例如,考虑以下重载集
struct Array<Element> { /// Inserts `newElement` at `self.endIndex`. public mutating func append(_ newElement: Element) /// Inserts the contents of `newElements`, in order, at /// `self.endIndex`. public mutating func append<S: SequenceType>(_ newElements: S) where S.Generator.Element == Element }
这些方法构成一个语义族,并且实参类型乍一看似乎截然不同。但是,当
Element
是Any
时,单个元素可以与元素序列具有相同的类型。var values: [Any] = [1, "a"] values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
为了消除歧义,更明确地命名第二个重载。
struct Array { /// Inserts `newElement` at `self.endIndex`. public mutating func append(_ newElement: Element) /// Inserts the contents of `newElements`, in order, at /// `self.endIndex`. public mutating func append<S: SequenceType>(**contentsOf** newElements: S) where S.Generator.Element == Element }
请注意,新名称如何更好地匹配文档注释。在这种情况下,编写文档注释的行为实际上引起了 API 作者对该问题的注意。