这章的内容都围绕这几个准则:避免多余,避免歧义,倾向于与简洁的隐式声明,除非是会显式的可以提高可读性、减少引发歧义的可能性。

编译器警告

如果作者可以避免编译器的警告,应该消除编译器警告。
一个常见的例外是被废弃的 API。如果写的是组件,虽然这组被废弃的 API 自身已经不使用,但是对外会需要兼容旧的版本,所以还是保留一段时间的废弃的 API。这种编译器警告是可以接受的。

初始化方法

Swift 会为结构体按照自身的成员自动生成一个非 public 的初始化方法。如果这个初始化方法刚好适合,就不用再自己声明。
如果初始化方法来自实现字面量初始化协议 ExpressibleBy**Literal ,永远不要直接调用这个初始化方法,使用字面量初始化。

  1. struct Kilometers: ExpressibleByIntegerLiteral {
  2. init(integerLiteral value: Int) {
  3. // ...
  4. }
  5. }
  6. let k1: Kilometers = 10 // ✅
  7. let k2 = 10 as Kilometers // ✅
  8. let k = Kilometers(integerLiteral: 10) // ❌

其他初始化方法也不要直接调用 .init,因为是可省略的。
只有在把初始化方法当做闭包传递,和在使用元类型信息初始化时才能直接调用 .init

  1. let type = lookupType(context) // 这里的
  2. let x = type.init(arguments)
  3. let x = makeValue(factory: MyType.init)

属性

如果是一个只有 get 的计算属性,忽略 get:

  1. var totalCost: Int {
  2. return items.sum { $0.cost }
  3. }
  4. var totalCost: Int {
  5. get {
  6. return items.sum { $0.cost }
  7. }
  8. }

使用类型的简写

数组、字典、optional 都有简写的形式,在编译器允许的情况都使用简写的形式:[Element]、[Key: Value]、Wrapped? 。他们的完整写法是:Array、Dictionary、 Optional。也有一些时候编译器要求完整写法,比如 Array<Element>.Index 就不能使用简写的形式:

  1. func enumeratedDictionary<Element>(
  2. from values: [Element],
  3. start: Array<Element>.Index? = nil
  4. ) -> [Int: Element] {
  5. // ...
  6. }
  1. func enumeratedDictionary<Element>(
  2. from values: Array<Element>,
  3. start: Optional<Array<Element>.Index> = nil
  4. ) -> Dictionary<Int, Element> {
  5. // ...
  6. }

Void 是空的 tuple () 的别名,从实现角度说两者是一样的东西。在函数声明中,返回值只会用 Void 表示,而不会用 (),当然带有 func 关键字的函数声明会省略 Void 的返回声明。
空的参数则总是用 () 表示,不会使用 Void。

  1. func doSomething() {
  2. // ...
  3. }
  4. let callback: () -> Void
  1. func doSomething() -> Void {
  2. // ...
  3. }
  4. func doSomething2() -> () {
  5. // ...
  6. }
  7. let callback: () -> ()

Optional

不用使用 sentinel value,比如返回 -1 表示不存在。
Optional 表示的是一个没有错误的值,里面可能是一个值或者这个值不存在。比如查找一个值是否在结合中,没有找到是一个正常的结果,在预料中。当不存在这个值的时候不应该返回一个 error。

  1. func index(of thing: Thing, in things: [Thing]) -> Int? {
  2. // ...
  3. }
  4. if let index = index(of: thing, in: lotsOfThings) {
  5. // Found it.
  6. } else {
  7. // Didn't find it.
  8. }
  1. func index(of thing: Thing, in things: [Thing]) -> Int {
  2. // ...
  3. }
  4. let index = index(of: thing, in: lotsOfThings)
  5. if index != -1 {
  6. // Found it.
  7. } else {
  8. // Didn't find it.
  9. }

如果只有一种明显的错误状态,Optional 也可以用来表示错误的发生,当然这个时候这个唯一出错的原因对于使用的人必须够明显。
比如将一个字符串转换为整型,失败的原因不言而喻。

  1. struct Int17 {
  2. init?(_ string: String) {
  3. // ...
  4. }
  5. }

如果判断值是不是为空使用 != nil 判断:

  1. if value != nil {
  2. print("value was not nil")
  3. }
  4. if let _ = value {
  5. print("value was not nil")
  6. }

Error 类型

Error 使用在有有多种错误状态时。
抛出错误比在返回值里返回错误逻辑更加清楚,程序也可以更好的分离关注点。

  1. struct Document {
  2. enum ReadError: Error {
  3. case notFound
  4. case permissionDenied
  5. case malformedHeader
  6. }
  7. init(path: String) throws {
  8. // ...
  9. }
  10. }
  11. do {
  12. let document = try Document(path: "important.data")
  13. } catch Document.ReadError.notFound {
  14. // ...
  15. } catch Document.ReadError.permissionDenied {
  16. // ...
  17. } catch {
  18. // ...
  19. }

这样的设计也迫使调用者考虑如何处理可能发生的错误:

  • 把调用包在 do-catch 里,决定如何处理错误。

  • 把函数声明标记为 throws,把错误再往外抛。

  • 如果可以忽略产生的错误使用 try?
    常规情况禁止使用 try!,这等于调用了 fatalError 但是没有提供任何有效的信息。只有在单元测试的代码里可以使用。

强制解包和强制类型映射

这两种都是非常不推荐使用的方式,大多数时候都代表着不好的编程习惯。除非是周围的代码可以明显的看出这样操作是安全的,并且应该添加注释说明为什么是安全的。当然在单元测试了例外。

Implicitly Unwrapped Optionals

IUO 也是天生的不安全,所以应该尽量避免。一些几种情况例外。

  • UI 对象对应的 @IBOutlet 属性( @IBOutlet var button: UIButton!),使用 IUO 可以降低每次使用时都需要考虑 optional 的情况。因为在载入 UI 时这些属性会被系统初始化,只是不在 swift 的初始化时发生。

  • 与 OC 桥接时某些缺少正确声明 nullability 的属性。

  • 单元测试。

访问权限

省略默认的访问权限( internal )。
禁止在 extension 前声明访问权限。每一个成员应该单独声明。

  1. extension String {
  2. public var isUppercase: Bool {
  3. // ...
  4. }
  5. public var isLowercase: Bool {
  6. // ...
  7. }
  8. }
  1. public extension String {
  2. var isUppercase: Bool {
  3. // ...
  4. }
  5. var isLowercase: Bool {
  6. // ...
  7. }
  8. }

类型嵌套和命名空间

Swift 允许 enum、struct、class 可以嵌套声明。如果某个类型和另外一个类型属于从属的关系,应该考虑使用嵌套的声明。比如某个类型的 error :

  1. class Parser {
  2. enum Error: Swift.Error {
  3. case invalidToken(String)
  4. case unexpectedEOF
  5. }
  6. func parse(text: String) throws {
  7. // ...
  8. }
  9. }
  1. class Parser {
  2. func parse(text: String) throws {
  3. // ...
  4. }
  5. }
  6. enum ParseError: Error {
  7. case invalidToken(String)
  8. case unexpectedEOF
  9. }

Swift 还不允许协议嵌套声明,所以 protocol 不使用这条规则。
如果需要使用类似命名空间的功能存储一组静态常量、变量,使用 enum。因为 enum 天生没有 instance,所以不会出现其他干扰。

  1. enum Dimensions {
  2. static let tileMargin: CGFloat = 8
  3. static let tilePadding: CGFloat = 4
  4. static let tileContentSize: CGSize(width: 80, height: 64)
  5. }
  1. struct Dimensions {
  2. private init() {}
  3. static let tileMargin: CGFloat = 8
  4. static let tilePadding: CGFloat = 4
  5. static let tileContentSize: CGSize(width: 80, height: 64)
  6. }

提前返回使用 guard

  1. func discombobulate(_ values: [Int]) throws -> Int {
  2. guard let first = values.first else {
  3. throw DiscombobulationError.arrayWasEmpty
  4. }
  5. guard first >= 0 else {
  6. throw DiscombobulationError.negativeEnergy
  7. }
  8. var result = 0
  9. for value in values {
  10. result += invertedCombobulatoryFactory(of: value)
  11. }
  12. return result
  13. }
  1. func discombobulate(_ values: [Int]) throws -> Int {
  2. if let first = values.first {
  3. if first >= 0 {
  4. var result = 0
  5. for value in values {
  6. result += invertedCombobulatoryFactor(of: value)
  7. }
  8. return result
  9. } else {
  10. throw DiscombobulationError.negativeEnergy
  11. }
  12. } else {
  13. throw DiscombobulationError.arrayWasEmpty
  14. }
  15. }

for-where 循环

如果整个 for 循环在函数体顶部只有一个 if 判断,使用 for where 替换:

  1. for item in collection where item.hasProperty {
  2. // ...
  3. }
  4. for item in collection {
  5. if item.hasProperty {
  6. // ...
  7. }
  8. }

Switch 中的 fallthrough

Switch 中如果有几个 case 都对应相同的逻辑,case 使用逗号连接条件,而不是使用 fallthrough:

  1. switch value {
  2. case 1: print("one")
  3. case 2...4: print("two to four")
  4. case 5, 7: print("five or seven")
  5. default: break
  6. }
  7. switch value {
  8. case 1: print("one")
  9. case 2: fallthrough
  10. case 3: fallthrough
  11. case 4: print("two to four")
  12. case 5: fallthrough
  13. case 7: print("five or seven")
  14. default: break
  15. }

换句话说,不存在 case 中只有 fallthrough 的情况。如果 case 中有自己的代码逻辑再 fallthrough 是合理的。

Pattern Matching(模式匹配)

需要被匹配的的元素前都会被带有 let 或者 var 关键字修饰。也有语法糖可以直接把 let、var 写在 case 的后面,但是禁止这样做,因为会引发潜在的歧义。原因是如果元素没有 let、var 修饰,表示不参与匹配,直接取一个已知的值。

  1. enum DataPoint {
  2. case unlabeled(Int)
  3. case labeled(String, Int)
  4. }
  5. let label = "goodbye"
  6. // 这里的 label 的值没有修饰 let,是前面声明的字符串 "goodbye",所以这里并不会走到这个 case,
  7. // 除非值是 DataPoint.labeled("goodbye", x)
  8. switch DataPoint.labeled("hello", 100) {
  9. case .labeled(label, let value): // 这里等于是 .labeled("goodbye", let value):
  10. // ...
  11. }
  12. // 每一个需要匹配的元素前面都应该添加关键字 let/var :
  13. switch DataPoint.labeled("hello", 100) {
  14. case .labeled(let label, let value):
  15. // ...

因此,禁止把修饰符提前:

  1. switch DataPoint.labeled("hello", 100) {
  2. case let .labeled(label, value):
  3. // ...
  4. }

模式匹配的时候如果变量名称和参数名称一致,可以省略参数标签:

  1. enum BinaryTree<Element> {
  2. indirect case subtree(left: BinaryTree<Element>, right: BinaryTree<Element>)
  3. case leaf(element: Element)
  4. }
  5. switch treeNode {
  6. case .subtree(let left, let right):
  7. // ...
  8. case .leaf(let element):
  9. // ...
  10. }

下面可以省略前面的标签:

  1. switch treeNode {
  2. case .subtree(left: let left, right: let right):
  3. // ...
  4. case .leaf(element: let element):
  5. // ...
  6. }

Tuple Pattern

如果使用 tuple pattern 赋值的时候左边的元素不能有标签,否则容易引起误解:

  1. let (a, b) = (y: 4, x: 5.0)
  2. let (x: a, y: b) = (y: 4, x: 5.0)

数字、字符串字面量

数字、字符串字面量在 swift 没有一个具体的类型。比如 5 并不是一个 Int,只有在赋值给一个实现了 ExpressibleByIntegerLiteral 协议的变量的时候才代表 Int。字符串字面量也是类似,可以是 String、Character 或者 UnicodeScalar。
因此如果上下文不足以推断字面量的类型时,需要声明赋值变量的类型。

  1. // 如果其他上下文约束,x1 会被推断为 Int
  2. let x1 = 50
  3. // 这样显式的表明了类型是 Int32.
  4. let x2: Int32 = 50
  5. let x3 = 50 as Int32
  6. // 如果其他上下文约束,y1 会被推断为 String.
  7. let y1 = "a"
  8. // 这样显式的表明了类型是 Character.
  9. let y2: Character = "a"
  10. let y3 = "a" as Character
  11. // 这样显式的表明了类型是 UnicodeScalar.
  12. let y4: UnicodeScalar = "a"
  13. let y5 = "a" as UnicodeScalar
  14. func writeByte(_ byte: UInt8) {
  15. // ...
  16. }
  17. // 这里的 50 会被推断为 UInt8
  18. writeByte(50)

由此也可能引发一些隐藏的 bug。如果是直接字面量赋值,在赋值的时候编译器就会坚持类型匹配。

  1. // 报错: 整型字面量 '9223372036854775808' 转为 'Int64' 会溢出
  2. let a = 0x8000_0000_0000_0000 as Int64
  3. // 报错: 'String' 类型不能强制转为 'Character'
  4. let b = "ab" as Character

如果使用初始化的方式则会有难以察觉的 bug:

  1. // 这里会先创建一个整型,然后再转为 UInt64。这个长度在 UInt64 是够的,但是在 Int 里会溢出
  2. let a1 = UInt64(0x8000_0000_0000_0000)
  3. // 这里调用的是 `Character.init(_: String)` 初始化函数,因此 "a" 被先转为了
  4. // 字符串类型,这样浪费了性能。
  5. let b = Character("a")
  6. // 和上面类似,先转换成了字符串类型,在运行时才会报错
  7. let c = Character("ab")

Playground Literal

图形化的字面量 #colorLiteral(...)#imageLiteral(...)#fileLiteral(...) 只能用在 playground 里,禁止在生产环境中使用。

自定义操作符

自定义缺少共同认知的操作符会显著的降低代码可读性。应该尽量避免自定义操作符,除非这个操作符针对特定问题有着广泛的认可。比如定义两个两个矩阵结构体的 * 就很合理。