Swift

Swift API 设计指南

深入理解 Swift API 设计的核心原则和最佳实践,包括命名规范、使用约定和参数设计。

核心原则

使用点的清晰性是首要目标

方法和属性只声明一次,但会被反复使用。设计 API 时,必须确保使用时清晰简洁。仅阅读声明往往不够,应始终检查实际使用场景,确保在上下文中表达清晰。

// 清晰的 API 设计
employees.remove(at: x)

// 不清晰:我们是在删除 x 还是在位置 x 删除?
employees.remove(x)

清晰性优于简洁性

虽然 Swift 代码可以很紧凑,但实现最少字符数并非目标。Swift 代码的简洁性是强类型系统和自然减少样板代码的副作用。

// 好:清晰表达意图
public mutating func remove(_ member: Element) -> Element?
allViews.remove(cancelButton)

// 差:不必要的冗余
public mutating func removeElement(_ member: Element) -> Element?
allViews.removeElement(cancelButton)

为每个声明编写文档注释

编写文档时获得的洞察会深刻影响设计。如果难以用简单术语描述 API 的功能,可能设计了错误的 API。

/// 返回包含 `self` 中相同元素的反转视图。
func reversed() -> ReverseCollection<Self>

/// 在 `self` 开头插入 `newHead`。
mutating func prepend(_ newHead: Int)

/// 如果非空,移除并返回 `self` 的第一个元素;否则返回 `nil`。
mutating func popFirst() -> Element?

命名规范

促进清晰使用

包含避免歧义所需的所有词语

extension List {
  // 好:明确表示在指定位置删除
  public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)

// 差:可能暗示搜索并删除等于 x 的元素
employees.remove(x)

省略冗余词语

名称中的每个词都应在使用点传达重要信息。特别要省略仅重复类型信息的词语。

// 差:Element 没有增加有用信息
public mutating func removeElement(_ member: Element) -> Element?
allViews.removeElement(cancelButton)

// 好:更清晰
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)
}

补偿弱类型信息

当参数类型是 NSObjectAnyAnyObject 或基本类型(如 IntString)时,类型信息和上下文可能无法充分传达意图。在每个弱类型参数前添加描述其角色的名词。

// 模糊
func add(_ observer: NSObject, for keyPath: String)
grid.add(self, for: graphics)

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

追求流畅使用

方法和函数名称应形成语法化的英语短语

// 好:形成流畅的短语
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()

工厂方法以 "make" 开头

x.makeIterator()
factory.makeWidget(gears: 42, spindles: 14)

初始化器和工厂方法的第一个参数不应与基础名称形成短语

// 好:第一个参数不与基础名称形成短语
let foreground = Color(red: 32, green: 64, blue: 128)
let newPart = factory.makeWidget(gears: 42, spindles: 14)
let ref = Link(target: destination)

// 差:试图创建语法连续性
let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
let ref = Link(to: destination)

根据副作用命名函数和方法

无副作用的应读作名词短语:

x.distance(to: y)
i.successor()

有副作用的应读作祈使动词短语:

print(x)
x.sort()
x.append(y)

一致命名可变/非可变方法对

当操作自然地由动词描述时,使用动词的祈使式命名可变方法,并应用 "ed" 或 "ing" 后缀命名非可变对应方法:

// 可变方法
x.sort()
x.append(y)

// 非可变方法
z = x.sorted()
z = x.appending(y)

优先使用动词的过去分词(通常添加 "ed"):

/// 原地反转 `self`。
mutating func reverse()

/// 返回 `self` 的反转副本。
func reversed() -> Self

x.reverse()
let y = x.reversed()

当添加 "ed" 不符合语法时,使用动词的现在分词(添加 "ing"):

/// 从 `self` 中剥离所有换行符
mutating func stripNewlines()

/// 返回剥离所有换行符的 `self` 副本。
func strippingNewlines() -> String

s.stripNewlines()
let oneLine = t.strippingNewlines()

当操作自然地由名词描述时,使用名词命名非可变方法,并应用 "form" 前缀命名可变对应方法:

// 非可变
x = y.union(z)
j = c.successor(i)

// 可变
y.formUnion(z)
c.formSuccessor(&i)

善用术语

避免使用晦涩术语

如果更常见的词能同样传达含义,就不要使用晦涩术语。不要说 "epidermis" 如果 "skin" 能达到目的。

坚持既定含义

使用技术术语的唯一理由是它精确表达了某些否则会模糊或不清楚的内容。因此,API 应严格按照其公认含义使用术语。

// 好:使用既定术语
let sorted = numbers.sorted()
let reversed = items.reversed()

// 差:不要为既定术语创造新含义
let ordered = numbers.ordered() // 令人困惑

避免缩写

缩写实际上是术语,因为理解取决于正确地将其翻译为非缩写形式。任何使用的缩写都应能通过网络搜索轻松找到其含义。

// 好:完整词语
var identifier: String
var minimum: Int
var maximum: Int

// 差:非标准缩写
var id: String  // 可接受,因为广泛使用
var min: Int    // 可接受,因为广泛使用
var max: Int    // 可接受,因为广泛使用

拥抱先例

不要为了完全的初学者而牺牲与现有文化的一致性。

// 好:使用既定术语
Array  // 而非 List
sin(x) // 而非 verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)

约定

通用约定

记录非 O(1) 的计算属性复杂度

人们通常假设属性访问不涉及重大计算。当这个假设可能被违反时,务必提醒他们。

/// 集合中的元素数量。
///
/// - Complexity: O(1)
var count: Int { get }

/// 集合中满足条件的元素数量。
///
/// - Complexity: O(n),其中 n 是集合的长度。
func count(where predicate: (Element) -> Bool) -> Int

优先使用方法和属性而非自由函数

自由函数仅在特殊情况下使用:

// 没有明显的 self 时
min(x, y, z)

// 函数是无约束泛型时
print(x)

// 函数语法是既定领域符号的一部分时
sin(x)

遵循大小写约定

类型和协议名称使用 UpperCamelCase,其他所有内容使用 lowerCamelCase

// 类型和协议
struct LinkedList<Element> { }
protocol Collection { }

// 其他
var currentIndex: Int
func makeIterator() -> Iterator

在美式英语中通常全大写的首字母缩写词应根据大小写约定统一大写或小写:

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

其他首字母缩写词应视为普通词:

var radarDetector: RadarScanner
var enjoysScubaDiving = true

方法可以共享基础名称

当方法具有相同的基本含义或在不同领域操作时,可以共享基础名称:

extension Shape {
  /// 如果 `other` 在 `self` 区域内返回 `true`;否则返回 `false`。
  func contains(_ other: Point) -> Bool { ... }
  
  /// 如果 `other` 完全在 `self` 区域内返回 `true`;否则返回 `false`。
  func contains(_ other: Shape) -> Bool { ... }
  
  /// 如果 `other` 在 `self` 区域内返回 `true`;否则返回 `false`。
  func contains(_ other: LineSegment) -> Bool { ... }
}

避免 "基于返回类型的重载",因为在类型推断存在时会导致歧义:

// 差:返回类型重载导致歧义
extension Box {
  func value() -> Int? { ... }
  func value() -> String? { ... }
}

参数

选择参数名称以服务文档

即使参数名称不出现在函数或方法的使用点,它们也起着重要的解释作用。

// 好:参数名称使文档易读
/// 返回包含 `self` 中满足 `predicate` 的元素的 `Array`。
func filter(_ predicate: (Element) -> Bool) -> [Element]

/// 用 `newElements` 替换给定的 `subRange` 元素。
mutating func replaceRange(_ subRange: Range<Index>, with newElements: [E])

// 差:参数名称使文档笨拙
/// 返回包含 `self` 中满足 `includedInResult` 的元素的 `Array`。
func filter(_ includedInResult: (Element) -> Bool) -> [Element]

利用默认参数简化常见用法

任何具有单一常用值的参数都是默认值的候选者。

// 没有默认值:冗长
let order = lastName.compare(
  royalFamilyName, options: [], range: nil, locale: nil)

// 有默认值:简洁
let order = lastName.compare(royalFamilyName)

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

将带默认值的参数放在参数列表末尾

没有默认值的参数通常对方法的语义更重要,并在调用方法时提供稳定的初始使用模式。

参数标签

当参数无法有效区分时省略所有标签

min(number1, number2)
zip(sequence1, sequence2)

在执行值保留类型转换的初始化器中省略第一个参数标签

Int64(someUInt32)

extension String {
  // 将 `x` 转换为给定基数的文本表示
  init(_ x: BigInt, radix: Int = 10)
}

text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16)

在 "收窄" 类型转换中,建议使用描述收窄的标签:

extension UInt32 {
  /// 创建具有指定 `value` 的实例。
  init(_ value: Int16)  // 扩展,无标签
  
  /// 创建具有 `source` 最低 32 位的实例。
  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)   // 不要关闭?关闭一个 Bool?
words.split(12)       // 分割数字 12?

标记所有其他参数

func move(from start: Point, to end: Point)
x.move(from: x, to: y)

特殊说明

在生产代码中优先使用 #fileID 而非 #filePath

#fileID 节省空间并保护开发者隐私。仅在需要完整路径时使用 #filePath

func log(_ message: String, file: String = #fileID, line: Int = #line) {
  print("\(file):\(line) - \(message)")
}

总结

Swift API 设计指南的核心在于:

  1. 清晰性至上: 使用点的清晰性是最重要的目标
  2. 流畅表达: 代码应读起来像自然的英语短语
  3. 一致性: 遵循既定的命名约定和模式
  4. 文档驱动: 编写文档能揭示设计问题
  5. 角色优先: 根据角色而非类型命名
  6. 简化使用: 利用默认参数和清晰的标签

遵循这些指南能确保你的 API 感觉像是更大 Swift 生态系统的自然组成部分,为用户提供清晰、一致的开发体验。

在 GitHub 上编辑

上次更新于