原文鏈接:https://swift.org/documentation/api-design-guidelines/
基本原則
代碼明晰是你主要目標。實體諸如方法和屬性,一次聲明,往往會被使用多次。故設計APIs時盡量使之清晰并且簡練。評估某個API設計是否合理,單從閱讀其聲明并不足以下結論,往往需要在真實示例下,才能確保它在上下文中是清晰正確的。
明晰優先于簡練。盡管Swift代碼可以書寫的非常緊湊簡練,但實現最少代碼量并非是我們的目標。Swift代碼的簡練,只是
強類型系統和自然降低功能樣板
產生的附加效果而已。-
為每個聲明書寫文檔注釋。通過書寫文檔獲得的經驗見解會對你的設計產生深遠的影響,所以不要忽視之。
如果你無法使用簡潔的術語描述你的APIs功能,那么你可能設計了錯誤的APIs。
詳情
使用Swift的Markdown語法。
-
以摘要開始,描述聲明實體的功能。一般的,通過其聲明和摘要信息,API會被人清晰的理解。
/// Returns `self`的"view"逆向集合。即包含相同元素,但順序相反。 func reversed() -> ReverseCollection
專注于摘要:這是最為重要的部分。很多優秀的文檔注釋只包含一個優質的摘要,別無它物。
盡可能使用單句片段,并以句點結束。不要使用完整冗長的句子。
-
描述方法或函數的功能和返回值,忽略無意義的null功能和Viod返回:
/// 在
self
起始處插入newHead
。
mutating func prepend(_ newHead: Int)
/// 返回一個包含head
并由self
跟隨的列表。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? ...
-
(可選),連續使用一個或多個段落和項目符號項。段落用空行分隔并使用完整的句子。
/// 將`items`中每個元素的文本表示,執行標準輸出。 ← 摘要 /// ← 空行 /// 每個元素`x`的文本表示均由`String(x)`表達式生成 ← 補充說明 /// /// - 參數 separator: 元素之間的打印文本。 ? /// - 參數 terminator: 結尾打印的文本 ? 參數部分 /// ? /// - 備注: 若不要在結尾新起一行, ///則置`terminator: ""`即可。 ? ///- 其它關聯: `CustomDebugStringConvertible`, ? 命令符號 /// `CustomStringConvertible`, `debugPrint`. ? public func print( _ items: Any..., separator: String = " ", terminator: String = "\n")
- 在適當場景,在摘要之外使用可識別的符號文檔標記元素添加信息。
- 使用符號命令語法,使用已識別的項目符號。
流行的IDE工具(如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|
命名
提高明晰方法
-
為了便于閱讀代碼,在進行命名時,要涵蓋所有必須的單詞,以避免歧義。
例如,一個方法:在集合中刪除給定位置的元素。
code1:? extension List { public mutating func remove(at position: Index) -> Element } employees.remove(at: x)
如果我們從方法簽名中省略單詞
at
,則可能使讀者認為該方法是用于搜索并刪除等于x的元素,而不是使用x來指示要刪除的元素的位置。code2:? employees.remove(x) // 不清晰:是刪除x嗎?
-
務必忽略不必須的單詞。命名中的每個單詞都應在使用場景中傳達重要的信息。
有時需要更多的單詞來闡明意圖或消除歧義,但應省略那些眾所周知的冗余詞。特別是要省略那些僅為重復類型信息的單詞。
code:? public mutating func removeElement(_ member: Element) -> Element? allViews.removeElement(cancelButton)
在該示例中,
Element
在調用場景中沒有傳達任何重要信息,故該API可優化為:code:? public mutating func remove(_ member: Element) -> Element? allViews.remove(cancelButton) // clearer
有時,重復類型信息對于避免歧義是有必要的。但通常而言,最好使用描述參數角色而不是其類型的單詞。有關詳細信息,請參閱下一項。
-
根據角色來命名變量,參數和關聯類型,而不是根據類型約束。
code:? var string = "Hello" protocol ViewController { associatedtype ViewType : View } class ProductionLine { func restock(from widgetFactory: WidgetFactory) }
以這種方式重新定位類型名稱無法優化清晰度和表現力。相反,努力選擇一個表達實體角色的名稱反而更好。
code: ? var greeting = "Hello" protocol ViewController { associatedtype ContentView : View } class ProductionLine { func restock(from supplier: WidgetFactory) }
如果關聯類型與其協議約束緊密綁定以使協議名稱為角色,請通過將
Protocol
附加到協議名稱來避免沖突:protocol Sequence { associatedtype Iterator : IteratorProtocol } protocol IteratorProtocol { ... }
-
補償弱類型信息以闡明參數的作用。
尤其當參數類型是
NSObject
,Any
,AnyObject
或諸如Int或String的基本類型
時,類型信息和在使用處的上下文可能無法完全傳達意圖。 在此示例中,聲明可能是明確的,但使用點是模糊的:code:? func add(_ observer: NSObject, for keyPath: String) grid.add(self, for: graphics) // 模糊
為了恢復清晰度,在每個弱類型參數前面加上描述其角色的名詞:
code:? func addObserver(_ observer: NSObject, forKeyPath path: String) grid.addObserver(self, forKeyPath: graphics) // 清晰
力求流暢使用
-
使用標準英語語法規則來命名方法和函數。
code: ? 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”
code: ? 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)。
例如,這些調用方法的第一個參數不會作為與基本名稱相同的短語的一部分讀取:
code: ? let foreground = Color(red: 32, green: 64, blue: 128) let newPart = factory.makeWidget(gears: 42, spindles: 14) let ref = Link(target: destination)
在以下示例中,API作者嘗試使用第一個參數創建語法連續性:
code: ? let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128) let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14) let ref = Link(to: destination)
實際上,此準則以及參數標簽的準則意味著第一個參數將具有標簽,除非調用正在執行值保留類型轉換。
let rgbForeground = RGBColor(cmykForeground)
-
根據副作用命名函數和方法。
那些沒有副作用的函數和方法應該讀作是一個名詞詞組。如
x.distance(to: y)
,i.successor().
那些有副作用的函數和方法應該讀作是一個命令式的動詞短語。如
print(x)
,x.sort()
,x.append(y)
。-
命名名稱一致的Mutating/nonmutating方法對。變異方法通常具有一個類似語義的非突變變體,但返回新值而不是就地更新實例。
- 當操作方法由動詞自然描述時,使用動詞對變異方法進行命名,而應用“ed”或“ing”后綴來命名對應的其非變異方法。
Mutating Nonmutating x.sort() z = x.sorted() x.append(y) z = x.appending(y)
* 更傾向于使用[動詞的過去分詞](https://en.wikipedia.org/wiki/Participle)命名非變異變體(通常附加`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`前綴來命名其對應的變異方法。
| 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
。 - 其它類型,諸如屬性、變量、常量應以名詞來命名。
更好的使用術語
名詞 - 在特定領域或專業中具有精確、專門意義的詞或短語。
如果一個更常見的詞語同樣傳達了相同意義,則避免使用模糊術語 。如果
皮膚
能夠闡述您的目的,請不要說表皮
。藝術品是一種必不可少的溝通工具,但只應用于捕捉原本會丟失的重要意義。-
如果您使用藝術術語,請堅持使用它既定的意義。
使用技術術語而不是更常見的單詞,其唯一原因是它可以更精確地表達一些本來會模棱兩可或不清楚的東西。因此,API應嚴格使用術語。
- 勿使專家驚訝。如果我們為眾所周知的術語發明了新的含義,任何已經熟悉它的人都會感到驚訝或憤怒。
- 不要迷惑初學者:任何試圖學習該術語的人都可能會進行網絡搜索并找到其傳統的原始意義。
-
避免使用縮寫。縮寫,尤其是非標準的縮寫,實際上是術語,因為理解依賴于正確地將它們翻譯成非縮寫形式。
您使用的任何縮寫的含義,都可在網絡找到。即眾所周知的含義。
-
擁抱先例。不要以犧牲與現有文化的一致性為代價來為初學者優化術語。
命名連續數據結構為
Array
比使用簡單術語(如List)更好,即使初學者可能更容易理解List
的含義。Array
是現代計算的基礎,因此每個程序員都應該知道 - 或者很快就會學到 -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
-
當方法具有相同的基本含義或在不同的域中操作時,方法可以共用基本名稱。
例如,以下方案值得推薦,因為這些方法的功能基本上是相同的:
code: ? extension Shape { /// 返回 `true` ,假如 `other` 點在`self`面積之內. func contains(_ other: Point) -> Bool { ... } /// 返回 `true` 假如 `other` 圖形完全在 `self`之內. func contains(_ other: Shape) -> Bool { ... } /// 返回 `true` 假如 `other`線條在 `self`之內. func contains(_ other: LineSegment) -> Bool { ... } }
由于幾何類型和集合是不同的域,因此在同一程序中也可以:
code: ? extension Collection where Element : Equatable { /// 返回 `true` 假如 `self` 包含一個同 `sought`相同的元素. func contains(_ sought: Element) -> Bool { ... } }
當然,這些索引方法具有不同的語義,并且應該以不同的方式命名:
code: ? extension Database { /// 重建數據庫的搜索索引 func index() { ... } /// 返回給定表中的第n行 func index(_ n: Int, inTable: TableID) -> TableRow { ... } }
最后,避免“在返回類型上重載”,因為它會在存在類型推斷時引起歧義:
code: ? extension Box { /// 返回存儲在`self`中的`Int`值,否則,返回`nil` func value() -> Int? { ... } /// 返回存儲在`self`中的`String `值,否則,返回`nil` func value() -> String? { ... } }
參數
func move(from start: Point, to end: Point)
-
選擇參數名稱以供文檔注釋。即使參數名稱沒有出現在函數或方法的使用點,它們也起著重要的解釋作用。
選擇這些名稱可以使文檔易于閱讀。例如,以下這些名稱使文檔閱讀理解更加自然:
code: ? /// 返回滿足`predicate`斷言的,并包含`self`的元素集合 func filter(_ predicate: (Element) -> Bool) -> [Generator.Element] /// 以`newElements`替換給定 `subRange`范圍的集合。 mutating func replaceRange(_ subRange: Range, with newElements: [E])
當然,以下例子使文檔變得笨拙和不合語法:
code: ? /// 返回滿足`includedInResult `斷言的,并包含`self`的元素集合 . func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element] /// 以`with `替換給定 `r `范圍的集合。 mutating func replaceRange(_ r: Range, with: [E])
-
一般場景時,合理利用默認參數。某一參數在大多數場景下都是某個固定值,比較適合設置默認參數。
默認參數通過隱藏不相關的信息來提高可讀性。例如:
code: ? let order = lastName.compare( royalFamilyName, options: [], range: nil, locale: nil)
可以更為簡潔:
let order = lastName.compare(royalFamilyName)
默認參數通常比使用方法集更可取,因為它們會降低理解API的認知負擔。
code: ? extension String { /// ...description... public func compare( _ other: String, options: CompareOptions = [], range: Range? = nil, locale: Locale? = nil ) -> Ordering }
上述方案可能并不簡單,但相比方法集,足夠簡潔:
code: ? 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 }
方法集合的每個成員都需要單獨的文檔注釋,并由用戶理解。用戶需要完全理解它們,才能選擇最優方法。 偶爾也會出現令人驚訝的問題 - 例如,
foo(bar:nil)
和foo()
并不總是同等的 - 文檔繁瑣,差異卻微小。 使用含默認值單一方法可提供極其優越的編程體驗。 默認參數應放置在參數列表的末尾。沒有默認值的參數通常對于方法的語義更為重要,并且在調用方法時提供穩定的初始使用模式。
參數標簽
func move(from start: Point, to end: Point)
x.move(from: x, to: y)
-
在無法有效區分參數時省略所有標簽
如:
min(number1, number2)
,zip(sequence1, sequence2).
-
在執行
值保留類型轉換
的構造器中,省略第一個參數標簽。第一個參數應該始終是轉換的來源:
extension String { // 將`x`轉換為給定基數中的文本表示 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) ← Widening, so no label /// 創建一個具有最低32位“source”的實例 `source`. init(truncating source: UInt64) /// 創建近似于`valueToApproximate`的實例 init(saturating valueToApproximate: UInt64) }
值保持類型轉換
是單態的,即原始值的每個差異均會導致結果值的差異。 例如,從Int8到Int64的轉換是值保留的,因為每個不同的Int8值都轉換為不同的Int64值;但是,在相反方向上的轉換不能保留值:Int64具有比Int8中表示的值更多的可能值。注意:檢索原始值的能力與轉換是否有保留值無關。
-
當第一個參數構成介詞短語的一部分時,給它設置一個參數標簽。參數標簽通常應該從介詞開始,如
x.removeBoxes(havingLength: 12)
。當前兩個參數表示單個抽象的一部分時會出現異常:
code: ? a.move(toX: b, y: c) a.fade(fromRed: b, green: c, blue: d)
這種情況,在介詞后添加參數標簽,以保持抽象概念清晰。
code: ? 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) Don't dismiss? Dismiss a Bool?
words.split(12) Split the number 12?
```
另請注意,可以省略含默認值的參數。在這種情況下,不要形成語法短語的一部分,因此它們應始終具有標簽。
- 為其它所有參數添加參數標簽。
特別說明
-
在API中為tuple元組成員添加參數標簽,命名閉包參數。
這些名稱具有很好的解釋能力,可以從文檔注釋中引用,并提供對元組成員的訪問。
/// 確保我們有requestedCapacity最后一個元素的唯一引用的存儲單元。 /// /// 如果需要更多存儲空間,則調用`allocate` 。且分配的字節數`bygerCount`等于最大數。 /// /// - 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<Void> ) -> (reallocated: Bool, capacityChanged: Bool)
用于閉包參數的名稱,應如頂級函數的參數名稱一樣。在閉包參數調用處不應出現參數標簽。
-
使用不受約束的多態性(例如
Any
,AnyObject
和無約束的通用參數
)時要格外小心,以避免重載集中出現歧義。如考慮重載集:
? struct Array { /// 在 `self.endIndex`處插入`newElement`. public mutating func append(_ newElement: Element) /// 在`self.endIndex`順序插入 `newElements`內容。 public mutating func append(_ newElements: S) where S.Generator.Element == Element }
這些方法形成一個語義簇,并且參數類型明顯不同。但是,當Element為Any時,單個元素可以與元素序列具有相同的類型。
```
?
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
```
為消除歧義,可以更明確地命名第二個重載方法。
```
?
struct Array {
/// 在 `self.endIndex`處插入`newElement`.
public mutating func append(_ newElement: Element)
/// 在`self.endIndex`順序插入 `newElements`內容
public mutating func append(contentsOf newElements: S)
where S.Generator.Element == Element
}
```
注:如何命名以更好地匹配文檔注釋。實際上是在編寫文檔注釋時,得到了API作者的注意。
更多
- 紕漏之處,歡迎斧正。
- 更多內容請關注公眾號:
IT互聯網自習室
。