合理使用术语(Terminology)
-
如果一个常用的词可以清楚地表达意图,就不要使用晦涩难懂的术语。用 “skin” 就可以达到你的目的的话,就别用 “epidermis”了。术语是一种非常必要的交流工具,但是应该用在其他常用语无法表达关键信息的时候。
-
如果你确实要使用术语,请确保它已被明确定义
使用术语的唯一理由是,用其他常用词会表意不清或造成歧义。因此,API 应该严格依据某个术语被广泛接受的释义来进行命名。- 不要让专家惊讶:如果我们重新定义了某个术语,那会使熟知它的人感到惊讶甚至是愤怒。
- 不要让初学者困惑:初学某个术语的人通常都会上网搜索它的传统释义。
-
避免缩写。缩写,特别是不标准的缩写,已经算是一种术语了,因为要理解它的意思必须正确地把它翻译成完整版本才行。
你使用的所有缩写,必须可以很轻易的上网查到它的意思。
-
有例可循。不要为一个新人去优化术语,而不遵守现有的规范。
将一个线性的数据结构命名为Array
比一些更简单的词(譬如List
)要好,尽管List
对新手来说更易于理解。因为数组在现代计算机体系中是个非常基础的概念,每个程序员都已经知道或者能够很快地学会它。总之,请使用那些为程序员所熟知的术语,这样当人们搜索和询问时就能得到回应。
在一些特定的编程领域,譬如数学运算方面,广为人知的sin(x)
就比解释性的短语(如verticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAngle(x)
)要好得多。注意,在这种情况下,“有例可循”的优先级大于指南中的“不要使用缩写”,哪怕完整的词是sine
。毕竟“sin(x)”已经被程序员们用了几十年了,更被数学家们用了几个世纪了。
约定
通用约定
-
标注那些复杂度不是 O(1) 的计算属性。人们总是假定存取属性是不需要什么计算的,因为他们已经把属性保存为心智模型( mental model)了。
-
尽量使用方法和属性,而不是自由函数(全局函数)。自由函数仅适用于一些特定情况:
- 当没有明显的
self
:min(x, y, z)
- 当函数是无约束的范型(unconstrained generic):
print(x)
- 当函数句法(syntax)是权威认证的领域标记的一部分:
sin(x)
- 当没有明显的
-
遵守拼写约定(case conventions)。类型和协议用首字母大写驼峰命名法(UpperCamelCase)命名,其它的都用首字母小写(lowerCamelCase)驼峰命名法。
在英语中都是大写字母的首字母缩略词(Acronyms and initialisms)需要根据首字母情况统一成全大写或者全小写:
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? { ... }
}
参数
func move(from start: Point, to end: Point)
-
选择能服务于文档(documentation)编写的参数。虽然参数名不在方法的调用处显示,但它们起到了非常重要的解释说明作用。
选择能使文档通俗易懂的参数名。比如,下面这些参数名就是文档读上去很自然:
/// 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)
一般来说,默认参数比方法族(method families)更可取,因为它减轻了 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
}
方法族中的每个方法都需要被分别注释和被使用者理解。决定使用哪个方法之前,使用者必须理解所有方法,并且偶尔会对它们之间的关系感到惊讶,譬如,foo(bar: nil)
和foo()
并不总是同义的,从几乎相同的文档和注释中去区分这些微笑差异是十分乏味的。而一个有默认参数的方法将提供上等的编码体验。
- 尽量把默认参数放在参数列表的最后,没有默认值的参数通常对于方法的语义来说是必要的,在方法被调用的时候提供了稳定的初始形态。
参数标签
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)
然而,在一个“narrowing”的转换中(译者注:范围变窄的转换,譬如 Int64 转 Int32),用一个标签来表明范围变窄是推荐的做法。
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)
}
- 当第一个参数是介词短语的一部分时,给它一个参数标签。标签应该正常地以介词开头,譬如:
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) Don't dismiss? Dismiss a Bool?
words.split(12) Split the number 12?
注意,默认参数是可以被删除的,在这种情况下它们都不是短语的一部分,所以它们总是应该有标签。
- 给其它所有参数都加上标签。
特别说明
- 在 API 中给闭包参数和元组成员加上标签
这些名字有解释说明的作用,可以出现在文档注释中,并且给元组成员一个形象的入口。
/// 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)
用在闭包中时,虽然从技术上来说它们是参数标签,但你应该把它们当做参数名来选择和解释(文档中)。闭包在方法体中被调用时跟调用方法时是一致的,方法签名从一个不包含第一个参数的方法名开始:
allocate(byteCount: newCount * elementSize)
-
要特别注意那些不受约束的类型(譬如,
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<
S : SequenceType where S.Generator.Element == Element
>(newElements: S)
}
这些方法组成了一个语义族(semantic family),第一个参数的类型是明确的。但是当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 {
/// 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)
}
注意新命名是怎样更好地匹配文档注释的。在这种情况下,写文档注释时实际上也在提醒 API 作者自己注意问题。
网友评论