原文链接: https://swift.org/documentation/api-design-guidelines/
译者:Changkun Ou
2016-05-11 初稿
本文未经授权禁止转载
译者注:
Argument 和 Parameter 两个词在很多文献中均翻译为参数,这是一个历史遗留问题。
但实际上 Argument 专用于 Actual Argument(实际参数,实参),Parameter 专用于 Formal Parameter(形式参数,形参)。
本译文在上下文没有歧义的情况下均翻译为参数,在其他情况下使用实参和形参来对 Argument 和 Parameter 加以区分。
进一步阅读: ISO/IEC 9899 - 在这份标准中,第 3 页的 actual parameter 和第 6 页的 formal argument 两种说法均被弃用。
方法和属性这样的实体只声明一次,却会被重复调用。因此你在设计 API 时应尽可能使其简单明了。当评估某个设计时,只阅读声明往往是不够的,有时还需要检查它的使用样例,才能确保其在上下文中足够清晰。
一目了然比简洁更重要。尽管 Swift 代码可以非常简明,但是使用少量的字符使得代码变得简短并不是我们的目的。简洁的 Swift 代码,会成为强类型系统副作用,而同时也是自然地降低版面的重要特点。
给每个声明 编写文档注释 。编写文档会对你的设计产生深远的影响进而增加你的见解,所以不要闲置它们。
:warning:如果你不能简单的描述 API 功能,那么 你很有可能设计错了 API。
/// 返回 `self` 的 "view",其中包含顺序相反的相同元素。
func reversed() -> ReverseCollection
专注于摘要;这是最重要的部分。很多杰出的文档其实什么都没有,但是它们有很好的注释摘要。
如果可以, 使用单一的语句片段 并用句号结尾。不要写一个复杂的句子。
要描述一个函数或方法会干什么,以及会返回什么,省略那些没有意义的以及返回 Void
的说明。
/// 在 `self` 的起始处插入 `newHead` 。
mutating func prepend(newHead: Int)
/// 返回一个由 `head` 和 `self` 依次组成的 `List` 。
func prepending(head: Element) -> List
/// 如果不空,这返回`self`的第一个元素,
/// 否则返回 `nil`。
mutating func popFirst() -> Element?
注意:像 popFirst
等少数情况,摘要应该被多条语句按照不同的情况分成不同的片段。
/// 访问第 `index` 个元素。
subscript(index: Int) -> Element { get set }
/// 创建一个包含 `n` 个 `x` 的实例。
init(count n: Int, repeatedElement x: Element)
/// 支持任意位置高效插入删除元素的列表
struct List {
/// 元素从 `self` 开始,若 self 为空则为 `nil`
var first: Element?
...
/// Writes the textual representation of each ← 摘要
/// element of `items` to the standard output.
/// ← 空行
/// The textual representation for each item `x` ← 扩展描述
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed ⎫
/// between items. ⎟
/// - Parameter terminator: text to be printed ⎬ 参数区
/// at the end. ⎟
/// ⎭
/// - Note: To print without a trailing ⎫
/// newline, pass `terminator: ""` ⎟
/// ⎬ 标识与其相关的命令
/// - SeeAlso: `CustomDebugStringConvertible`, ⎟
/// `CustomStringConvertible`, `debugPrint`. ⎭
public func print(
items: Any..., separator: String = " ", terminator: String = "/n")
使用识别的 符号文档标记(markup) 元素 ,在适当的情况下将信息加到摘要之外。
熟知并使用符号命令语法识别零散的条目。像 Xcode 等主流开发工具为零散的条目提供了特殊的处理,其关键词如下:
Attention | Author | Authors | Bug | Complexity | Copyright | Date | Experiment | Important | Invariant | Note | Parameter | Parameters | Postcondition | Precondition | Remark | Requires | Returns | SeeAlso | Since | Throws | Todo | Version | Warning |
例:考虑一个移除集合中移除指定位置的元素的方法。
:white_check_mark:
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
如果我们省略方法名中的 at
,它可能暗示读者该方法会进行搜索并删除集合中等于 x
的元素,而不是用 x
来指示元素在集合中的位置并删除这个元素。
:no_entry:️
employees.remove(x) // 不明确: 我们是在删除 x 吗?
更多的单词或许可以明确意图并消除歧义,但是那些众所周知的冗余信息都可以删掉。尤其是那些仅仅重复类型信息的单词。
:no_entry:️
public mutating func removeElement(member: Element) -> Element?
allViews.removeElement(cancelButton)
对于这种情况, Element
在调用处没有提供任何要点信息,如下 API 会更好。
:white_check_mark:
public mutating func remove(member: Element) -> Element?
allViews.remove(cancelButton) // 更明了
个别情况下,重复类型信息对于消除歧义是必要的,但一般来说,用一个词来表明参数作用而不是类型的词会更好一些,详见下一条。
:no_entry:️
var string = "Hello"
protocol ViewController {
associatedtype ViewType : View
}
class ProductionLine {
func restock(from widgetFactory: WidgetFactory)
}
重申一遍类型名其实并不能提升明确性和表现力。相反,你应该尽量选用那些表明实体作用的名字。
:white_check_mark:
var greeting = "Hello"
protocol ViewController {
associatedtype ContentView : View
}
class ProductionLine {
func restock(from supplier: WidgetFactory)
}
如果某个关联类型和它的协议联系非常紧密,导致它的协议名就是它自身的名字,那就给关联类型的名字加上 Type
避免冲突:
protocol Sequence {
associatedtype IteratorType : Iterator
}
当参数类型是 NSObject
、 Any
、 AnyObject
或者像 Int
、 String
这样的基本类型的时候,调用处的类型信息和上下文环境可能无法完全表明函数意图。在下面的例子中,它的声明可能明确,但在调用的地方意图不明。
:no_entry:️
func add(observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) // vague
为了恢复明确性,在每个弱类型参数前加一个名词用来描述它的角色。
:white_check_mark:
func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) // clear
:white_check_mark:
x.insert(y, at: z) “x, insert y at z”
x.subViews(havingColor: y) “x's subviews having color y”
x.capitalizingNouns() “x, capitalizing nouns”
:no_entry:️
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)。
举个例子,如下的调用短语都不包含第一实参。
:white_check_mark:
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
而下面这段代码,API 作者试图用第一实参创建符语法连续性的API:
:no_entry:️
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
事实上,本指南对参数的标签都做了这样的处理,这意味着第一实参都会包含一个标签,除非该方法完全只是用来做宽度相等的类型转换。
let rgbForeground = RGBColor(cmykForeground)
根据函数和方法的副作用进行命名
x.distance(to: y)
, i.successor()
。 print(x)
, x.sort()
, x.append(y)
。 始终给 一对可变方法和不可变方法命名 。一个可变方法通常会有一个与之对应的不可变方法,但是不可变方法会返回一个新的值而不是更新现有的实例。
当操作 被一个名词描述时 ,给可变方法使用 ed
或 ing
前缀来给动词添加祈使语气。
Mutating | Nonmutating | 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 reversed() -> 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 strippingNewlines() -> String
...
s.stripNewlines()
let oneLine = t.strippingNewlines()
当操作 被一个名词描述时 ,可给不可变方法使用名词并添加 form
前缀。
Nonmutating | Mutating | 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
)。
其它 类型、属性、变量和约束的命名都应该用名词 。
Term of Art(术语), 名词 : 一个词或短语,在特定的领域或行业中有更精确专业的意义。
如果一个常用的词可以清楚地表达意图,就不要使用晦涩难懂的术语。用 “skin” 就可以达到你的目的的话,就别用 “epidermis”了。术语是一种非常必要的交流工具,但是应该用在其他常用语无法表达关键信息的时候。
如果你确实要使用术语, 确保它已被明确定义 。
使用术语的唯一理由是,用其他常用词会表意不清或造成歧义。因此,API 应该严格依据某个术语被广泛接受的释义来进行命名。
所有缩写必须能轻易在互联网中搜索到它的意思。
将一个线性的数据结构命名为 Array
比一些更简单的词要好,如 List
。尽管 List
对新手来说更易于理解。因为Array(数组) 在现代计算机体系中是个非常基础的概念,每个程序员都已经知道或者能够很快地学会它。总之,请使用那些为程序员所熟知的术语,这样当人们搜索和询问时就能得到回应。
在一些特定的编程领域,譬如数学运算方面,广为人知的 sin(x)
就比解释性的短语 verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
要好得多。注意,在这种情况下,“有例可循”的优先级大于指南中的“不要使用缩写”,哪怕完整的词是 sine
。毕竟 sin(x)
已经被程序员们用了几十年了,更被数学家们用了好几个世纪了。
标注那些复杂度不是 O(1) 的计算型属性。人们总是假定存取属性是不需要什么计算的,因为他们把属性保存作为了心智模型。
尽量使用方法和属性,而不是普通函数。普通函数仅适用于一些特定情况:
self
: min(x, y, z)
print(x)
sin(x)
UpperCamelCase
)进行命名,其它则均为首字母小写( lowerCamelCase
)驼峰命名法。 缩写通常在美式英语中会根据首字母情况统一成全大写或者全小写:
var utf8Bytes: [UTF8.CodeUnit];
var isRepresentableAsASCII = true;
var userSMTPServer: SecureSMTPServer;
其它首字母缩略词当作普通单词处理即可:
var radarDetector: RadarScanner;
var enjoysScubaDiving = true;
例如,我们鼓励下面这种做法,因为这几个方法基本做了相同的事情:
:white_check_mark:
extension Shape {
/// Returns `true` iff `other` is within the area of `self`.
func contains(other: Point) -> Bool { ... }
/// Returns `true` iff `other` is entirely within the area of `self`.
func contains(other: Shape) -> Bool { ... }
/// Returns `true` iff `other` is within the area of `self`.
func contains(other: LineSegment) -> Bool { ... }
}
由于泛型和容器都有各自独立的范围,所以在同一个程序里这样使用也是可以的:
:white_check_mark:
extension Collection where Element : Equatable {
/// Returns `true` iff `self` contains an element equal to
/// `sought`.
func contains(sought: Element) -> Bool { ... }
}
然而,下面这些 index
方法有不同的语义,应该采用不同的命名:
:no_entry:️
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 { ... }
}
最后,你还应避免返回值重载,因为在类型推断时会产生歧义:
:no_entry:️
extension Box {
/// 如果有值,返回 `Int` 并保存在 `self`
/// 否则返回 `nil`
func value() -> Int? { ... }
/// 如果有值,返回 `String` 并保存在 `self`
/// 否则返回 `nil`
func value() -> String? { ... }
}
func move(from start: Point, to end: Point)
选择能使文档通俗易懂的参数名。比如,下面这些参数名就是文档读上去很自然:
:white_check_mark:
/// 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, with newElements: [E])
而这些就使文档难以理解且不合语法:
:no_entry:️
/// 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, with: [E])
通过隐藏无关信息,默认参数提高了代码可读性,例如:
:no_entry:️
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)
可以更加简洁:
:white_check_mark:
let order = lastName.compare(royalFamilyName)
提供默认参数比使用一类方法更可取,因为它减轻了 API 使用者的认知负担。
:white_check_mark:
extension String {
/// ...描述...
public func compare(
other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
}
上面的代码可能不够简洁,但它比如下的代码简洁多了:
:no_entry:️
extension String {
/// ...描述 1...
public func compare(other: String) -> Ordering
/// ... 描述 2...
public func compare(other: String, options: CompareOptions) -> Ordering
/// ... 描述 3...
public func compare(
other: String, options: CompareOptions, range: Range) -> Ordering
/// ... 描述 4...
public func compare(
other: String, options: StringCompareOptions,
range: Range, locale: Locale) -> Ordering
}
一类方法中的每个方法都需要被分别注释和被使用者理解。决定使用哪个方法之前,使用者必须理解所有方法,并且偶尔会对它们之间的关系感到惊讶——比如, foo(bar: nil)
和 foo()
并不总表示相同的方法——从几乎相同的文档和注释中去区分这些微笑差异是很无聊的。而一个有默认参数的方法将提升编码的体验。
func move(from start: Point, to end: Point)
x.move(from: x, to: y)
当不同参数不能被有效区分时,删除所有参数标签,如: min(number1, number2)
, zip(sequence1, sequence2)
。
当构造器完全只用作类型转换的时候,删除第一个参数标签,如: Int64(someUInt32)
。
第一个参数总应该是要被转换的东西。
extension String {
// 将 `x` 转换为给定进制下的文本表示
init(_ x: BigInt, radix: Int = 10) ← 注意起始的下划线
}
text = "值为: "
text += String(veryLargeNumber)
text += "十六进制为: "
text += String(veryLargeNumber, radix: 16)
然而,在缩小转换(Narrowing Conversion)中,用标签来表明范围变窄是推荐的做法。
extension UInt32 {
/// 创建一个精确类型的实例 `value`
init(_ value: Int16) ← 精度更宽,因此没有标签
/// 创建最低有 32 位的实例 `source`。
init(truncating source: UInt64)
/// 创建最接近描述 `valueToApproximate` 近似值的实例
init(saturating valueToApproximate: UInt64)
}
x.removeBoxes(havingLength: 12)
。 头两个参数各自相当于某个抽象的一部分的情况,是个例外:
:no_entry:️
a.move(toX: b, y: c)
a.fade(fromRed: b, green: c, blue: d)
在这种情况下,参数的标签应在介词之后,进而保证抽象概念清晰。
:white_check_mark:
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
x.addSubview(y)
。 这条指南暗示了如果第一个参数不是符合语法的短语的部分形式,那它应该有一个标签。
:white_check_mark:
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
注意,短语必须表达正确的意思非常重要。下面的短语是符合语法正确但单词表达了错误的意思。
:no_entry:️
view.dismiss(false) 不 dismiss? Dismiss 一个 Bool 值?
words.split(12) 分割数字 12?
注意,默认参数是可以被删除的,在这种情况下它们都不是短语的一部分,所以它们总是应该有标签。
这些名字有解释说明的能力,可以出现在文档注释中,并且给元组成员提供了表达渠道。
/// 确保我们至少持有并唯一引用 `requestedCapacity` 元素。
///
/// 若需要更多存储空间, `allocate` 会被调用
/// `byteCount` 等于最大对齐的内存单元数
///
/// - Returns:
/// - reallocated: 当且仅当分配新内存区为 `true`
/// - capacityChanged: 当且仅当 `capacity` 被更新时为 `true`
mutating func ensureUniqueStorage(
minimumCapacity requestedCapacity: Int,
allocate: (byteCount: Int) -> UnsafePointer<Void>
) -> (reallocated: Bool, capacityChanged: Bool)
尽管在在闭包中使用它们,从技术上来说是实参标签,但即便它们是形参时候,你还是应该在选择这些标签并在文档中使用。闭包在方法体中被调用时跟调用方法时是一致的,方法签名从一个不包含第一个参数的方法名开始:
allocate(byteCount: newCount * elementSize)
Any
, AnyObject
和一些不受约束的泛型参数),以防在重载时产生歧义。 举个例子,考虑如下重载:
:no_entry:️
struct Array {
/// 在 `self.endIndex` 插入 `newElement`
public mutating func append(newElement: Element)
/// 在 `self.endIndex` 顺序插入 `newElements` 的内容
public mutating func append<
S : SequenceType where S.Generator.Element == Element
>(newElements: S)
}
这些方法来自同一组语义,其第一个参数的类型是明确的。但是当 Element
是 Any
时,一个单独的元素和一个元素集合的类型相同。
:no_entry:️
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] 还是 [1, "a", 2, 3, 4]?
为了消除歧义,应将第二个重载方法的命名加以明确。
:white_check_mark:
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 where S.Generator.Element == Element
>(contentsOf newElements: S)
}
注意新的命名是如何更好地匹配文档的注释的。在这种情况下,写文档这种行为其实也在提醒 API 作者自己。