[译] 官方 Swift API 设计规范

没故事的卓同学 2018-09-21 17:38:05 2346

官方地址:API design guidelines

核心原则

  • 最重要的目标:每个元素都能够准确清晰的表达出它的含义。做出 API 设计、声明后要检查在上下文中是否足够清晰明白。

  • 清晰比简洁重要。虽然 swift 代码可以被写得很简短,但是让代码尽量少不是 swift 的目标。简洁的代码来源于安全、强大的类型系统和其他一些语言特性减少了不必要的模板代码。而不是主观上写出最少的代码。

  • 为每一个声明写注释文档。编写文档过程中获得的理解可以对设计产生深远的影响,所以不要回避拖延。

如果你不能很好的描述 API 的功能,很可能这个 API 的设计就是有问题的。

文档注释建议

使用 swift 支持的 markdown 语法。

从总结声明的实体开始。通常通过声明和总结一个 API 应该可以被完全理解。

/// Returns a "view" of self containing the same elements in 
/// reverse order. 
func reversed() -> ReverseCollection
  • 注意是总结,不是直白的描述。

  • 用一个简短的语句描述,不需要使用一个完整的句子。

  • 描述函数或者方法做了什么,返回值。如果没有返回值或者返回结果不重要可以省略。

/// 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?
  • 自定义subscript说明访问的是什么:

/// Accesses the `1 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 ← 总结 
/// 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")

强烈建议在注释里合适的地方使用定义好文档的标志。

  • Xcode 对以下列表项关键字会做特别的提示:

命名

让代码被正确使用

保证命名让使用的人不会产生歧义。

比如在集合中有一个方法,根据给定的位置移除元素:

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

如果在方法签名中省略了at,用户在使用的时候就会以为这是删除一个等于 x 的元素,而不是移除索引在 x 的元素:

×
employees.remove(x) // 不够清晰: 这里感觉像是移除 x


省略无用的词。命名中的每一个字都应该有意义。

如果使用者已经知道某些信息,那么这个信息在命名中就可以省略。尤其是重复的类型信息可以被省略。

X
public mutating func removeElement(_ member: Element) -> Element?

allViews.removeElement(cancelButton)

在这个例子中 Element没有传达出更多有效的信息。这样会好一点:

public mutating func remove(_ member: Element) -> Element?

allViews.remove(cancelButton) // 更清晰

有的时候重复类型信息可以避免歧义,但是通常用一个参数的角色命名比类型更有意义。


根据承担的角色命名变量、参数、关联类型,而不是它们的类型限制

X
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)
}

如果一个 associatedtype 的角色和类型刚好一样,为了避免冲突可以在后面加上 “Type”:

protocol Sequence { 
associatedtype IteratorType : Iterator 
}


声明参数的角色以消除弱类型信息。

特别是当一个参数的类型是 NSObject 、 Any、 AnyObject 或者像 Int 、 String 这种基础类型时,类型信息和使用时的上下文不能很好的传递出参数的用途。下面这个例子中声明虽然可以说是清晰的,但是使用的时候还是会有让人看不明白。

X
func add(_ observer: NSObject, for keyPath: String)

grid.add(self, for: graphics) 

为了能够重新表达清晰,在每个弱类型参数前加一个名词描述它的角色:

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
x.insert(y, position: z)
x.subViews(color: y)
x.nounCapitalize()

为了流畅度把后面的和方法名相关弱的参数换行也是可以接受的:

AudioUnit.instantiate
withdescription
options[.inProcess]completionHandlerstopProgressBar)

如果是创建型的工厂方法,用 “make” 开头。比如:x.makeIterator()。

如果是创建型的方法(init 和 make),那么第一个参数的标签不要考虑组合成一个句子。因为这样的函数名称已经知道是要创建一个实例,那么参数再用介词修饰让句子流畅显得多余。正确的示范: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)

如果为了句子的连贯性就会声明成下面这样(但是并不推荐这样做):

X
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
let ref = Link(to: destination)

还有一种第一个参数不会带标签的情况是保留了原有值的类型转换(比如 Int32 -> Int64 就保留了原有的值,但是如果 Int64 -> Int32 则需要在参数前有标签):

let rgbForeground = RGBColor(cmykForeground)

函数、方法命名时要参考自身的副作用。

  • 没有副作用的名字读起来应该像一个名词的短语。比如: x.distance(to: y), i.successor()。

  • 有副作用的读起来应该是一个祈使式的动词短语,比如:print(x), x.sort(), x.append(y)。

  • 对成对 Mutating/nonmutating 的方法命名要保持一致。一个 mutating 方法通常有一个类似名字的 nonmutating 方法,会返回一个新的值而不是修改自身的值。

如果描述操作的是一个动词,使用动词的祈使态表示 mutating,nonmutating 在动词后加上 “ed” 或 “ing” 表示。

| Mutating | Nonmutating |

| :--- | :--- |

| x.sort() | z = x.sorted() |

| x.append(y) | z = x.appending(y) |

When the operation is naturally described by a noun, use the noun for the nonmutating method and apply the “form” prefix to name its mutating counterpart. 如果描述操作的是一个名词,名词表示 nonmutating,mutating 则在前面加上 “form” 表示。

| Nonmutating | Mutating |

| -------------------- | --------------------- |

| x = y.union(z) | y.formUnion(z) |

| j = c.successor(i) | c.formSuccessor(&i) |

如果是一个 nonmutating 的布尔属性、方法读起来应该是一个判断。比如:x.isEmpty, line1.intersects(line2)。

表示是什么的 Protocol 读起来应该是一个名词。比如:Collection。

表示能力的 Protocol 后缀应该用 able、ible 或者 ing 修饰。比如:Equatable, ProgressReporting。

其他形式的类型、属性、变量、常量都应该用名词命名。

用好术语

如果有一个常见的词可以描述不要使用冷僻的术语。只有在他人能够理解这个术语的时候才去使用它。

严格的使用术语本来的含义。

使用技术术语的原因就是它比常用的词语能够更精确的表达含义,因此 API 应该严格按照其公认的含义使用术语。

  • 不要让专家感到惊讶:如果这个词出现在熟悉它的人面前,他还会觉得惊讶说明这个词的含义很可能被歪曲了。

  • 不要让新手感到迷茫:任何一个人如果想要了解这个术语通过一个普通的网络搜索就应该能够查到它的含义。

避免使用缩写,尤其是非标准的缩写。非标准的缩略语可能无法被其他人正确的理解。

使用的任何缩写的意思都应该很容易通过网络搜索查到。

尊重先例用法。不用因为新手的理解成本而改变原有用法。

如果是连续的数据结构,命名为 Array 比使用简化的术语 List 好。虽然初学者更容易理解 List 的含义,但是 Array 是现代计算的基础类型,每一个程序员都会知道它。使用一个更多程序员熟悉的词,这样在网络上能查找到的资料也会更多。

在一个特定的编程领域,比如数学,使用广泛的术语比声明一个解释性短语好,如 sin(x) 比verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x) 更可取。注意,在这种情况下,先例比避免使用缩写的指导原则更重要:尽管完整的单词是 sine,但 “sin(x)” 在程序员已经被使用了几十年,在数学家中更是数百年。

约定

通用约定

如果计算属性的复杂度不是 O(1) 在文档里说明。人们通常都会认为访问一个属性不需要沉重的计算,因为意识里认为只是获取一个存储过的值。当实际场景和人们设想的不一致时一定要提醒他们。

尽量使用方法和属性,而不是全局函数。全局函数只在一些特殊的场景里使用:

  • 使用时不需要 self 存在:

min(x, y, z)
  • 不限制类型的函数:

print(x)
  • 函数的使用方式已经是一个习惯用法:

sin(x)

类型和协议的命名首字母大写,其他的都是首字母小写。

  • 美式英语中首字母通常以大写出现的缩略词的所有字母大小写保持一致:

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 则有不同的含义,所以应该有不同的命名:

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


参数(Parameters)

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])

下面这样的命名在写文档的时候就会显得很奇怪,不符合语言习惯:

X
/// 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])

充分利用默认参数,尤其是可以简化大部分的使用情况时。只要是参数常用值就可以考虑配置成默认值。

默认参数通过隐藏不重要的信息提高了可读性。比如:

X
let order = lastName.compare( 
royalFamilyName, options: [], rangenil, locale: nil)

使用默认参数后可以变得更简洁:

let order = lastName.compare(royalFamilyName)

默认参数比用一组家族方法更好,因为使用者可以更容易的理解 API。

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

上面的定义也许不算简单,但是比下面这种方式还是简单的多了:

X
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) -> Ordering 
/// ...description 4... 
public func compare( 
_ other: String, options: StringCompareOptions, 
range: Range, locale: Locale) -> Ordering 
}

一组方法家族里的每一个方法都需要单独写文档。用户选择使用一个方法时,需要理解家族里的所有的方法,有的时候也会分不清这些方法的联系。一个利用默认参数方法理解起来体验就好多了。

倾向于把默认参数放在参数列表的尾部。没有默认值的参数通常对方法的语义更重要,不可省略的参数放在前面也可以使方法调用的方式更加一致。

参数标签(Argument Labels)

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) ← Note the initial underscore 
}

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) ←因为接收的是比自己容量小的数据,会保留精度,所以没有标签 
/// Creates an instance having the lowest 32 bits of source. 
init(truncating source: UInt64) ←可能丢失精度,使用truncating 提示用户 
/// Creates an instance having the nearest representable 
/// approximation of valueToApproximate. 
init(saturating valueToApproximate: UInt64) ←可能丢失精度,使用 valueToApproximate 提示用户 
}

当第一个参数构成介词短语的一部分时,给参数添加一个标签。这个标签通常以介词开头。比如:x.removeBoxes(havingLength: 12)。

有一种例外是前两个情况作为一个整体的参数。

X
a.move(toXbyc
a.fade(fromRedbgreencblued)

因为这些参数是有关联的一组数据,为了保持参数的一致,把介词移到方法名称里。

a.moveTo(xbyc
a.fadeFrom(redbgreencblued)

如果第一个参数标签可以构成语法上语句的一部分,把标签移到前面的基本名字里。比如:x.addSubview(y) 。

这条指导原则意味着如果第一个标签不构成语法上语句的一部分,它应该有一个标签。

view.dismiss(animated: false
let text = words.split(maxSplits: 12
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)

注意这里语法上的语句必须传达出正确的含义。下面这样声明虽然也能构成语法上的语句,但是表达的含义却是错误的。

X
view.dismiss(false) 这里是不要 dismiss 还是 dismiss 一个 Bool? 
words.split(12split 12 这个数字?

还要注意带有默认值的参数可以省略,并且在这种情况下不构成语法短语的一部分,因此它们应该总是有标签。

其他情况都需要声明参数标签。

其他的要求

给 API 中的 tuple 成员和闭包参数命名。

这些命名可以更好的解释含义,在文档注释中会被引用,也可以在访问 tuple 成员的时候被使用到。

/// 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 iff a new block of memory 
/// was allocated. 
/// - capacityChanged: true iff capacity was updated. 
mutating func ensureUniqueStorage
minimumCapacity requestedCapacity: Int, 
allocate: (_ byteCount: Int)
 -> UnsafePointer 
) -> (reallocated: Bool, capacityChanged: Bool)

闭包参数的命名规则和正常的函数参数规则一样,但是参数标签还不支持闭包。

特别注意没有类型限制(Any,AnyObject,没有类型限制的泛型参数)的多态使用,避免在重载中引发歧义。

比如下面的重载:

X
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(_ newElements: S)
    where S.Generator.Element 
== Element
}

上面的方法都共享同一个基本名字,参数的类型也有所不同。但是当 Element 的类型是 Any 时,element 会同时符合这两个方法的类型要求。

X
var values: [Any] = [1"a"]
values.append([234]) // [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(contentsOf newElements: S)
    where S.Generator.Element 
== Element
}

也要关注命名和文档注释里表达的意图是否匹配。这里改了新名字后和文档说明更加匹配了。