原文链接:https://swift.org/documentation/api-design-guidelines/

译者:Changkun Ou

2016-05-11 初稿
2016-05-12 初校

目录

译者注:

Argument 和 Parameter 两个词在很多文献中均翻译为参数,这是一个历史遗留问题。

但实际上 Argument 专用于 Actual Argument(实际参数,实参),Parameter 专用于 Formal Parameter(形式参数,形参)。

本译文在上下文没有歧义的情况下均翻译为参数,在其他情况下使用实参和形参来对 Argument 和 Parameter 加以区分。

进一步阅读ISO/IEC 9899 - 在这份标准中,第 3 页的 actual parameter 和第 6 页的 formal argument 两种说法均被弃用。

基本准则

  • 用法一目了然是你设计时最重要的目的。

方法和属性这样的实体只声明一次,却会被重复调用。因此你在设计 API 时应尽可能使其简单明了。当评估某个设计时,只阅读声明往往是不够的,有时还需要检查它的使用样例,才能确保其在上下文中足够清晰。

  • 一目了然比简洁更重要。 尽管 Swift 代码可以非常简明,但是使用少量的字符使得代码变得简短并不是我们的目的。简洁的 Swift 代码,会成为强类型系统副作用,而同时也是自然地降低版面的重要特点。

  • 给每个声明编写文档注释。编写文档会对你的设计产生深远的影响进而增加你的见解,所以不要闲置它们。

⚠️如果你不能简单的描述 API 功能,那么你很有可能设计错了 API。

  • 使用 Swift 形式地 Markdown —— Markup
  • 描述实体行为声明的摘要开始。通常,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

命名

明确用法

  • 在名字中包含所有需要的单词从而避免阅读代码的人陷入困惑

例:考虑一个移除集合中移除指定位置的元素的方法。


extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)

如果我们省略方法名中的 at,它可能暗示读者该方法会进行搜索并删除集合中等于 x 的元素,而不是用 x 来指示元素在集合中的位置并删除这个元素。

⛔️
employees.remove(x) // 不明确: 我们是在删除 x 吗?
  • 删除不需要的单词。名字中的每个单词都应在使用处表明主要信息。

更多的单词或许可以明确意图并消除歧义,但是那些众所周知的冗余信息都可以删掉。尤其是那些仅仅重复类型信息的单词。

⛔️
public mutating func removeElement(member: Element) -> Element?
allViews.removeElement(cancelButton)

对于这种情况,Element 在调用处没有提供任何要点信息,如下 API 会更好。


public mutating func remove(member: Element) -> Element?
allViews.remove(cancelButton) // 更明了

个别情况下,重复类型信息对于消除歧义是必要的,但一般来说,用一个词来表明参数作用而不是类型的词会更好一些,详见下一条。

  • 变量、参数、关联类型依据作用对其进行命名,而不是基于它们的类型。
⛔️
var string = "Hello"
protocol ViewController {
associatedtype ViewType : View
}
class ProductionLine {
func restock(from widgetFactory: WidgetFactory)
}

重申一遍类型名其实并不能提升明确性和表现力。相反,你应该尽量选用那些表明实体作用的名字。


var greeting = "Hello"
protocol ViewController {
associatedtype ContentView : View
}
class ProductionLine {
func restock(from supplier: WidgetFactory)
}

如果某个关联类型和它的协议联系非常紧密,导致它的协议名就是它自身的名字,那就给关联类型的名字加上Type避免冲突:

protocol Sequence {
associatedtype IteratorType : Iterator
}
  • 为弱类型信息的参数补充信息以表明参数的作用。

当参数类型是NSObjectAnyAnyObject或者像IntString这样的基本类型的时候,调用处的类型信息和上下文环境可能无法完全表明函数意图。在下面的例子中,它的声明可能明确,但在调用的地方意图不明。

⛔️
func add(observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) // vague

为了恢复明确性,在每个弱类型参数前加一个名词用来描述它的角色。


func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) // clear

为使用舒适而努力

  • 尽量使方法或函数名在调用的时,符合英语语法规范

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”
⛔️
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)

而下面这段代码,API 作者试图用第一实参创建符语法连续性的API:

⛔️
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)
    • 始终给一对可变方法和不可变方法命名。一个可变方法通常会有一个与之对应的不可变方法,但是不可变方法会返回一个新的值而不是更新现有的实例。
      • 当操作被一个名词描述时,给可变方法使用eding前缀来给动词添加祈使语气。
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) 的计算型属性。人们总是假定存取属性是不需要什么计算的,因为他们把属性保存作为了心智模型。

  • 尽量使用方法和属性,而不是普通函数。普通函数仅适用于一些特定情况:

  1. 当没有明显的 self
min(x, y, z)
  1. 当函数为无约束泛型:
print(x)
  1. 当函数的写法是公认记法的一部分:
sin(x)
  • 遵循常规用例。类型和协议用首字母大写驼峰命名法(UpperCamelCase)进行命名,其它则均为首字母小写(lowerCamelCase)驼峰命名法。

缩写通常在美式英语中会根据首字母情况统一成全大写或者全小写:

var utf8Bytes: [UTF8.CodeUnit];
var isRepresentableAsASCII = true;
var userSMTPServer: SecureSMTPServer;

其它首字母缩略词当作普通单词处理即可:

var radarDetector: RadarScanner;
var enjoysScubaDiving = true;
  • 当几个方法的基本意义相同,或者它们作用在明确的范围内时,可以共享同一个基本命名

例如,我们鼓励下面这种做法,因为这几个方法基本做了相同的事情:


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 { ... }
}

由于泛型和容器都有各自独立的范围,所以在同一个程序里这样使用也是可以的:


extension Collection where Element : Equatable {
/// Returns `true` iff `self` contains an element equal to
/// `sought`.
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 {
/// 如果有值,返回 `Int` 并保存在 `self`
/// 否则返回 `nil`
func value() -> Int? { ... }

/// 如果有值,返回 `String` 并保存在 `self`
/// 否则返回 `nil`
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, 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, with: [E])
  • 简化常见使用时利用默认参数的优点。任何有一个常用值的参数都可以使用默认参数。

通过隐藏无关信息,默认参数提高了代码可读性,例如:

⛔️
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)

可以更加简洁:


let order = lastName.compare(royalFamilyName)

提供默认参数比使用一类方法更可取,因为它减轻了 API 使用者的认知负担。


extension String {
/// ...描述...
public func compare(
other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
)
-> Ordering

}

上面的代码可能不够简洁,但它比如下的代码简洁多了:

⛔️
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)

头两个参数各自相当于某个抽象的一部分的情况,是个例外:

⛔️
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) 不 dismiss? Dismiss 一个 Bool 值?
words.split(12) 分割数字 12?

注意,默认参数是可以被删除的,在这种情况下它们都不是短语的一部分,所以它们总是应该有标签。

  • 给其它所有参数都加上标签

特殊说明

  • 在 API 中,给闭包参量和元组成员加上标签

这些名字有解释说明的能力,可以出现在文档注释中,并且给元组成员提供了表达渠道。

/// 确保我们至少持有并唯一引用 `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)
  • 特别注意那些不受约束的类型(如,AnyAnyObject 和一些不受约束的泛型参数),以防在重载时产生歧义。

举个例子,考虑如下重载:

⛔️
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)

}

这些方法来自同一组语义,其第一个参数的类型是明确的。但是当 ElementAny 时,一个单独的元素和一个元素集合的类型相同。

⛔️
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] 还是 [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 where S.Generator.Element == Element
>(contentsOf newElements: S)

}

注意新的命名是如何更好地匹配文档的注释的。在这种情况下,写文档这种行为其实也在提醒 API 作者自己。

文章目录
  1. 1. 目录
  2. 2. 基本准则
  3. 3. 命名
    1. 3.1. 明确用法
    2. 3.2. 为使用舒适而努力
    3. 3.3. 合理使用术语
  4. 4. 约定
    1. 4.1. 一般约定
    2. 4.2. 形参
    3. 4.3. 实参标签
  5. 5. 特殊说明