接上篇:Swift API 設(shè)計(jì)指南(上)
合理使用術(shù)語(yǔ)(Terminology)
如果一個(gè)常用的詞可以清楚地表達(dá)意圖,就不要使用晦澀難懂的術(shù)語(yǔ)。用 “skin” 就可以達(dá)到你的目的的話,就別用 “epidermis”了。術(shù)語(yǔ)是一種非常必要的交流工具,但是應(yīng)該用在其他常用語(yǔ)無(wú)法表達(dá)關(guān)鍵信息的時(shí)候。
-
如果你確實(shí)要使用術(shù)語(yǔ),請(qǐng)確保它已被明確定義
使用術(shù)語(yǔ)的唯一理由是,用其他常用詞會(huì)表意不清或造成歧義。因此,API 應(yīng)該嚴(yán)格依據(jù)某個(gè)術(shù)語(yǔ)被廣泛接受的釋義來(lái)進(jìn)行命名。- 不要讓專家驚訝:如果我們重新定義了某個(gè)術(shù)語(yǔ),那會(huì)使熟知它的人感到驚訝甚至是憤怒。
- 不要讓初學(xué)者困惑:初學(xué)某個(gè)術(shù)語(yǔ)的人通常都會(huì)上網(wǎng)搜索它的傳統(tǒng)釋義。
避免縮寫??s寫,特別是不標(biāo)準(zhǔn)的縮寫,已經(jīng)算是一種術(shù)語(yǔ)了,因?yàn)橐斫馑囊馑急仨氄_地把它翻譯成完整版本才行。
你使用的所有縮寫,必須可以很輕易的上網(wǎng)查到它的意思。
-
有例可循。不要為一個(gè)新人去優(yōu)化術(shù)語(yǔ),而不遵守現(xiàn)有的規(guī)范。
將一個(gè)線性的數(shù)據(jù)結(jié)構(gòu)命名為Array比一些更簡(jiǎn)單的詞(譬如List)要好,盡管List對(duì)新手來(lái)說(shuō)更易于理解。因?yàn)閿?shù)組在現(xiàn)代計(jì)算機(jī)體系中是個(gè)非?;A(chǔ)的概念,每個(gè)程序員都已經(jīng)知道或者能夠很快地學(xué)會(huì)它??傊?qǐng)使用那些為程序員所熟知的術(shù)語(yǔ),這樣當(dāng)人們搜索和詢問(wèn)時(shí)就能得到回應(yīng)。
在一些特定的編程領(lǐng)域,譬如數(shù)學(xué)運(yùn)算方面,廣為人知的sin(x)就比解釋性的短語(yǔ)(如verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x))要好得多。注意,在這種情況下,“有例可循”的優(yōu)先級(jí)大于指南中的“不要使用縮寫”,哪怕完整的詞是sine。畢竟“sin(x)”已經(jīng)被程序員們用了幾十年了,更被數(shù)學(xué)家們用了幾個(gè)世紀(jì)了。
約定
通用約定
標(biāo)注那些復(fù)雜度不是 O(1) 的計(jì)算屬性。人們總是假定存取屬性是不需要什么計(jì)算的,因?yàn)樗麄円呀?jīng)把屬性保存為心智模型( mental model)了。
-
盡量使用方法和屬性,而不是自由函數(shù)(全局函數(shù))。自由函數(shù)僅適用于一些特定情況:
- 當(dāng)沒(méi)有明顯的
self:min(x, y, z) - 當(dāng)函數(shù)是無(wú)約束的范型(unconstrained generic):
print(x) - 當(dāng)函數(shù)句法(syntax)是權(quán)威認(rèn)證的領(lǐng)域標(biāo)記的一部分:
sin(x)
- 當(dāng)沒(méi)有明顯的
遵守拼寫約定(case conventions)。類型和協(xié)議用首字母大寫駝峰命名法(UpperCamelCase)命名,其它的都用首字母小寫(lowerCamelCase)駝峰命名法。
在英語(yǔ)中都是大寫字母的首字母縮略詞(Acronyms and initialisms)需要根據(jù)首字母情況統(tǒng)一成全大寫或者全小寫:
var utf8Bytes: [UTF8.CodeUnit];
var isRepresentableAsASCII = true;
var userSMTPServer: SecureSMTPServer;
其它首字母縮略詞當(dāng)作普通單詞處理即可:
var radarDetector: RadarScanner;
var enjoysScubaDiving = true;
- 當(dāng)幾個(gè)方法的基本意義相同,或者它們作用在明確的范圍內(nèi)時(shí),可以共享同一個(gè)基本命名。
比如,下面的做法是被鼓勵(lì)的,因?yàn)檫@幾個(gè)方法基本做了相同的事情:
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 { ... }
}
由于范型和容器都有各自獨(dú)立的范圍,所以在同一個(gè)程序里像下面這樣使用也是可以的:
extension Collection where Element : Equatable {
/// Returns `true` iff `self` contains an element equal to
/// `sought`.
func contains(sought: Element) -> Bool { ... }
}
然而,如下這些index方法有不同的語(yǔ)義,應(yīng)該采用不同的命名:
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 { ... }
}
最后,避免參數(shù)重載,因?yàn)檫@在類型推斷時(shí)會(huì)產(chǎn)生歧義:
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? { ... }
}
參數(shù)
func move(from start: Point, to end: Point)
-
選擇能服務(wù)于文檔(documentation)編寫的參數(shù)。雖然參數(shù)名不在方法的調(diào)用處顯示,但它們起到了非常重要的解釋說(shuō)明作用。
選擇能使文檔通俗易懂的參數(shù)名。比如,下面這些參數(shù)名就是文檔讀上去很自然:
/// 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])
而下面這些就使文檔難以理解且不合語(yǔ)法:
/// 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])
- 當(dāng)默認(rèn)參數(shù)能簡(jiǎn)化常見(jiàn)調(diào)用的時(shí)候好好利用它。任何有一個(gè)常用值的參數(shù)都可以使用默認(rèn)參數(shù)。
通過(guò)隱藏次要信息,默認(rèn)參數(shù)提高了代碼可讀性,比如:
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)
可以更加簡(jiǎn)潔:
let order = lastName.compare(royalFamilyName)
一般來(lái)說(shuō),默認(rèn)參數(shù)比方法族(method families)更可取,因?yàn)樗鼫p輕了 API 使用者的認(rèn)知負(fù)擔(dān)。
extension String {
/// ...description...
public func compare(
other: String, options: CompareOptions = [],
range: Range? = nil, locale: Locale? = nil
) -> Ordering
}
上面的代碼可能不算簡(jiǎn)單,但它比如下的代碼簡(jiǎn)單多了:
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
}
方法族中的每個(gè)方法都需要被分別注釋和被使用者理解。決定使用哪個(gè)方法之前,使用者必須理解所有方法,并且偶爾會(huì)對(duì)它們之間的關(guān)系感到驚訝,譬如,foo(bar: nil)和foo()并不總是同義的,從幾乎相同的文檔和注釋中去區(qū)分這些微笑差異是十分乏味的。而一個(gè)有默認(rèn)參數(shù)的方法將提供上等的編碼體驗(yàn)。
- 盡量把默認(rèn)參數(shù)放在參數(shù)列表的最后,沒(méi)有默認(rèn)值的參數(shù)通常對(duì)于方法的語(yǔ)義來(lái)說(shuō)是必要的,在方法被調(diào)用的時(shí)候提供了穩(wěn)定的初始形態(tài)。
參數(shù)標(biāo)簽
func move(from start: Point, to end: Point)
x.move(from: x, to: y)
- 當(dāng)不同參數(shù)不能被很好地區(qū)分時(shí),刪除所有參數(shù)標(biāo)簽,譬如:
min(number1, number2)
zip(sequence1, sequence2)
-
當(dāng)構(gòu)造器完全只是用作類型轉(zhuǎn)換的時(shí)候,刪除第一個(gè)參數(shù)標(biāo)簽,譬如:
Int64(someUInt32)。
第一個(gè)參數(shù)應(yīng)該是轉(zhuǎn)換的源頭。
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)
然而,在一個(gè)“narrowing”的轉(zhuǎn)換中(譯者注:范圍變窄的轉(zhuǎn)換,譬如 Int64 轉(zhuǎn) Int32),用一個(gè)標(biāo)簽來(lái)表明范圍變窄是推薦的做法。
extension UInt32 {
/// Creates an instance having the specified `value`.
init(_ value: Int16) ← Widening, so no label
/// Creates an instance having the lowest 32 bits of `source`.
init(truncating source: UInt64)
/// Creates an instance having the nearest representable
/// approximation of `valueToApproximate`.
init(saturating valueToApproximate: UInt64)
}
- 當(dāng)?shù)谝粋€(gè)參數(shù)是介詞短語(yǔ)的一部分時(shí),給它一個(gè)參數(shù)標(biāo)簽。標(biāo)簽應(yīng)該正常地以介詞開(kāi)頭,譬如:
x.removeBoxes(havingLength: 12)
頭兩個(gè)參數(shù)各自相當(dāng)于某個(gè)抽象的一部分的情況,是個(gè)例外:
a.move(toX: b, y: c)
a.fade(fromRed: b, green: c, blue: d)
在這種情況下,為了保持抽象清晰,參數(shù)標(biāo)簽從介詞后面開(kāi)始。
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)
-
另外,如果第一個(gè)參數(shù)是符合語(yǔ)法規(guī)范的短語(yǔ)的一部分,刪除它的標(biāo)簽,在方法名后面加上前導(dǎo)詞,譬如:
x.addSubview(y)。
這條指南暗示了如果第一個(gè)參數(shù)不是符合語(yǔ)法規(guī)范的短語(yǔ)的一部分,它就應(yīng)該有個(gè)標(biāo)簽。
view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)
注意,短語(yǔ)必須表達(dá)正確的意思,這非常重要。如下的短語(yǔ)是符合語(yǔ)法的,但它們表達(dá)錯(cuò)誤了。
view.dismiss(false) Don't dismiss? Dismiss a Bool?
words.split(12) Split the number 12?
注意,默認(rèn)參數(shù)是可以被刪除的,在這種情況下它們都不是短語(yǔ)的一部分,所以它們總是應(yīng)該有標(biāo)簽。
- 給其它所有參數(shù)都加上標(biāo)簽。
特別說(shuō)明
- 在 API 中給閉包參數(shù)和元組成員加上標(biāo)簽
這些名字有解釋說(shuō)明的作用,可以出現(xiàn)在文檔注釋中,并且給元組成員一個(gè)形象的入口。
/// 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<Void>
) -> (reallocated: Bool, capacityChanged: Bool)
用在閉包中時(shí),雖然從技術(shù)上來(lái)說(shuō)它們是參數(shù)標(biāo)簽,但你應(yīng)該把它們當(dāng)做參數(shù)名來(lái)選擇和解釋(文檔中)。閉包在方法體中被調(diào)用時(shí)跟調(diào)用方法時(shí)是一致的,方法簽名從一個(gè)不包含第一個(gè)參數(shù)的方法名開(kāi)始:
allocate(byteCount: newCount * elementSize)
-
要特別注意那些不受約束的類型(譬如,
Any,AnyObject和一些不受約束的范型參數(shù)),以防在重載時(shí)產(chǎn)生歧義。
譬如,考慮如下重載:
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
>(newElements: S)
}
這些方法組成了一個(gè)語(yǔ)義族(semantic family),第一個(gè)參數(shù)的類型是明確的。但是當(dāng)Element 是Any時(shí),一個(gè)單獨(dú)的元素和一個(gè)元素集合的類型是一樣的。
var values: [Any] = [1, "a"]
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?
為了避免歧義,讓第二個(gè)重載方法的命名更加明確。
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)
}
注意新命名是怎樣更好地匹配文檔注釋的。在這種情況下,寫文檔注釋時(shí)實(shí)際上也在提醒 API 作者自己注意問(wèn)題。