1. 程式人生 > >iOS 官方 Swift API 設計規範

iOS 官方 Swift API 設計規範

核心原則

  1. 最重要的目標:每個元素都能夠準確清晰的表達出它的含義。做出 API 設計、聲明後要檢查在上下文中是否足夠清晰明白。
  2. 清晰比簡潔重要。雖然 swift 程式碼可以被寫得很簡短,但是讓程式碼儘量少不是 swift 的目標。簡潔的程式碼來源於安全、強大的型別系統和其他一些語言特性減少了不必要的模板程式碼。而不是主觀上寫出最少的程式碼。
  3. 為每一個宣告寫註釋文件。編寫文件過程中獲得的理解可以對設計產生深遠的影響,所以不要回避拖延。

如果你不能很好的描述 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

省略無用的詞。命名中的每一個字都應該有意義。 如果使用者已經知道某些資訊,那麼這個資訊在命名中就可以省略。尤其是重複的型別資訊可以被省略。

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

allViews.removeElement(cancelButton)

在這個例子中 Element沒有傳達出更多有效的資訊。這樣會好一點:

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

如果一個 associatedtype 的角色和型別剛好一樣,為了避免衝突可以在後面加上 “Type”:

protocol Sequence { 
associatedtype IteratorType : Iterator 
}

宣告引數的角色以消除弱型別資訊。

特別是當一個引數的型別是 NSObject 、 Any、 AnyObject 或者像 Int 、 String 這種基礎型別時,型別資訊和使用時的上下文不能很好的傳遞出引數的用途。下面這個例子中宣告雖然可以說是清晰的,但是使用的時候還是會有讓人看不明白。

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

為了流暢度把後面的和方法名相關弱的引數換行也是可以接受的:

AudioUnit.instantiate( with: description, options: [.inProcess],completionHandler: stopProgressBar)

如果是建立型的工廠方法,用 “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)

如果為了句子的連貫性就會宣告成下面這樣(但是並不推薦這樣做):

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

函式、方法命名時要參考自身的副作用

  1. 沒有副作用的名字讀起來應該像一個名詞的短語。比如: x.distance(to: y), i.successor()。
  2. 有副作用的讀起來應該是一個祈使式的動詞短語,比如:print(x), x.sort(), x.append(y)。
  3. 對成對 Mutating/nonmutating 的方法命名要保持一致。一個 mutating 方法通常有一個類似名字的 nonmutating 方法,會返回一個新的值而不是修改自身的值。

如果描述操作的是一個動詞,使用動詞的祈使態表示 mutating,nonmutating 在動詞後加上 “ed” 或 “ing” 表示。

Mutating Nonmutating
x.sort() z = x.sorted()
x.append(y) z = x.appending(y)

如果描述操作的是一個名詞,名詞表示 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 應該嚴格按照其公認的含義使用術語。

  1. 不要讓專家感到驚訝:如果這個詞出現在熟悉它的人面前,他還會覺得驚訝說明這個詞的含義很可能被歪曲了。
  2. 不要讓新手感到迷茫:任何一個人如果想要了解這個術語通過一個普通的網路搜尋就應該能夠查到它的含義。

避免使用縮寫,尤其是非標準的縮寫。非標準的縮略語可能無法被其他人正確的理解。

使用的任何縮寫的意思都應該很容易通過網路搜尋查到。

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

如果是連續的資料結構,命名為 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 則有不同的含義,所以應該有不同的命名:

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

下面這樣的命名在寫文件的時候就會顯得很奇怪,不符合語言習慣:

❌ 
/// 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 { 
/// ...description... 
public func compare( 
_ other: String, options: CompareOptions = [], 
range: Range? = 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) -> 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)。

有一種例外是前兩個情況作為一個整體的引數。

❌ 
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) split 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,沒有型別限制的泛型引數)的多型使用,避免在過載中引發歧義。 比如下面的過載:

❌
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 會同時符合這兩個方法的型別要求。

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

也要關注命名和文件註釋裡表達的意圖是否匹配。這裡改了新名字後和文件說明更加匹配了。