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

编译器警告

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

初始化方法

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

let k1: Kilometers = 10                          // ✅
let k2 = 10 as Kilometers                        // ✅

let k = Kilometers(integerLiteral: 10)           // ❌
其他初始化方法也不要直接调用 .init,因为是可省略的。 只有在把初始化方法当做闭包传递,和在使用元类型信息初始化时才能直接调用 .init
✅
let type = lookupType(context)   // 这里的
let x = type.init(arguments)

let x = makeValue(factory: MyType.init)

属性

如果是一个只有 get 的计算属性,忽略 get:
✅
var totalCost: Int {
  return items.sum { $0.cost }
}

❌
var totalCost: Int {
  get {
    return items.sum { $0.cost }
  }
}

使用类型的简写

数组、字典、optional 都有简写的形式,在编译器允许的情况都使用简写的形式:[Element]、[Key: Value]、Wrapped? 。他们的完整写法是:Array、Dictionary<Key, Value>、 Optional。也有一些时候编译器要求完整写法,比如 Array<Element>.Index 就不能使用简写的形式:
✅
func enumeratedDictionary<Element>(
  from values: [Element],
  start: Array<Element>.Index? = nil
) -> [Int: Element] {
  // ...
}
❌
func enumeratedDictionary<Element>(
  from values: Array<Element>,
  start: Optional<Array<Element>.Index> = nil
) -> Dictionary<Int, Element> {
  // ...
}
Void 是空的 tuple () 的别名,从实现角度说两者是一样的东西。在函数声明中,返回值只会用 Void 表示,而不会用 (),当然带有 func 关键字的函数声明会省略 Void 的返回声明。 空的参数则总是用 () 表示,不会使用 Void。
✅
func doSomething() {
  // ...
}

let callback: () -> Void
❌
func doSomething() -> Void {
  // ...
}

func doSomething2() -> () {
  // ...
}

let callback: () -> ()

Optional

不用使用 sentinel value,比如返回 -1 表示不存在。 Optional 表示的是一个没有错误的值,里面可能是一个值或者这个值不存在。比如查找一个值是否在结合中,没有找到是一个正常的结果,在预料中。当不存在这个值的时候不应该返回一个 error。
✅
func index(of thing: Thing, in things: [Thing]) -> Int? {
  // ...
}

if let index = index(of: thing, in: lotsOfThings) {
  // Found it.
} else {
  // Didn't find it.
}
❌
func index(of thing: Thing, in things: [Thing]) -> Int {
  // ...
}

let index = index(of: thing, in: lotsOfThings)
if index != -1 {
  // Found it.
} else {
  // Didn't find it.
}
如果只有一种明显的错误状态,Optional 也可以用来表示错误的发生,当然这个时候这个唯一出错的原因对于使用的人必须够明显。 比如将一个字符串转换为整型,失败的原因不言而喻。
✅
struct Int17 {
  init?(_ string: String) {
    // ...
  }
}
如果判断值是不是为空使用 != nil 判断:
✅
if value != nil {
  print("value was not nil")
}
❌
if let _ = value {
  print("value was not nil")
}

Error 类型

Error 使用在有有多种错误状态时。 抛出错误比在返回值里返回错误逻辑更加清楚,程序也可以更好的分离关注点。
✅
struct Document {
  enum ReadError: Error {
    case notFound
    case permissionDenied
    case malformedHeader
  }

  init(path: String) throws {
    // ...
  }
}

do {
  let document = try Document(path: "important.data")
} catch Document.ReadError.notFound {
  // ...
} catch Document.ReadError.permissionDenied {
  // ...
} catch {
  // ...
}
这样的设计也迫使调用者考虑如何处理可能发生的错误:

强制解包和强制类型映射

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

Implicitly Unwrapped Optionals

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

访问权限

省略默认的访问权限( internal )。 禁止在 extension 前声明访问权限。每一个成员应该单独声明。
✅
extension String {
  public var isUppercase: Bool {
    // ...
  }

  public var isLowercase: Bool {
    // ...
  }
}
❌
public extension String {
  var isUppercase: Bool {
    // ...
  }

  var isLowercase: Bool {
    // ...
  }
}

类型嵌套和命名空间

Swift 允许 enum、struct、class 可以嵌套声明。如果某个类型和另外一个类型属于从属的关系,应该考虑使用嵌套的声明。比如某个类型的 error :
✅
class Parser {
  enum Error: Swift.Error {
    case invalidToken(String)
    case unexpectedEOF
  }

  func parse(text: String) throws {
    // ...
  }
}
❌
class Parser {
  func parse(text: String) throws {
    // ...
  }
}

enum ParseError: Error {
  case invalidToken(String)
  case unexpectedEOF
}
Swift 还不允许协议嵌套声明,所以 protocol 不使用这条规则。 如果需要使用类似命名空间的功能存储一组静态常量、变量,使用 enum。因为 enum 天生没有 instance,所以不会出现其他干扰。
✅
enum Dimensions {
  static let tileMargin: CGFloat = 8
  static let tilePadding: CGFloat = 4
  static let tileContentSize: CGSize(width: 80, height: 64)
}
❌
struct Dimensions {
  private init() {}

  static let tileMargin: CGFloat = 8
  static let tilePadding: CGFloat = 4
  static let tileContentSize: CGSize(width: 80, height: 64)
}

提前返回使用 guard

✅
func discombobulate(_ values: [Int]) throws -> Int {
  guard let first = values.first else {
    throw DiscombobulationError.arrayWasEmpty
  }
  guard first >= 0 else {
    throw DiscombobulationError.negativeEnergy
  }

  var result = 0
  for value in values {
    result += invertedCombobulatoryFactory(of: value)
  }
  return result
}
❌
func discombobulate(_ values: [Int]) throws -> Int {
  if let first = values.first {
    if first >= 0 {
      var result = 0
      for value in values {
        result += invertedCombobulatoryFactor(of: value)
      }
      return result
    } else {
      throw DiscombobulationError.negativeEnergy
    }
  } else {
    throw DiscombobulationError.arrayWasEmpty
  }
}

for-where 循环

如果整个 for 循环在函数体顶部只有一个 if 判断,使用 for where 替换:
✅
for item in collection where item.hasProperty {
  // ...
}

❌
for item in collection {
  if item.hasProperty {
    // ...
  }
}

Switch 中的 fallthrough

Switch 中如果有几个 case 都对应相同的逻辑,case 使用逗号连接条件,而不是使用 fallthrough:
✅
switch value {
case 1: print("one")
case 2...4: print("two to four")
case 5, 7: print("five or seven")
default: break
}

❌
switch value {
case 1: print("one")
case 2: fallthrough
case 3: fallthrough
case 4: print("two to four")
case 5: fallthrough
case 7: print("five or seven")
default: break
}
换句话说,不存在 case 中只有 fallthrough 的情况。如果 case 中有自己的代码逻辑再 fallthrough 是合理的。

Pattern Matching(模式匹配)

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

let label = "goodbye"

// 这里的 label 的值没有修饰 let,是前面声明的字符串 "goodbye",所以这里并不会走到这个 case,
// 除非值是 DataPoint.labeled("goodbye", x)
switch DataPoint.labeled("hello", 100) {
case .labeled(label, let value): // 这里等于是 .labeled("goodbye", let value):
  // ...
}

// 每一个需要匹配的元素前面都应该添加关键字 let/var :
switch DataPoint.labeled("hello", 100) {
case .labeled(let label, let value):
  // ...
因此,禁止把修饰符提前:
❌
switch DataPoint.labeled("hello", 100) {
case let .labeled(label, value):
  // ...
}
模式匹配的时候如果变量名称和参数名称一致,可以省略参数标签:
✅
enum BinaryTree<Element> {
  indirect case subtree(left: BinaryTree<Element>, right: BinaryTree<Element>)
  case leaf(element: Element)
}

switch treeNode {
case .subtree(let left, let right):
  // ...
case .leaf(let element):
  // ...
}
下面可以省略前面的标签:
❌
switch treeNode {
case .subtree(left: let left, right: let right): 
  // ...
case .leaf(element: let element):
  // ...
}

Tuple Pattern

如果使用 tuple pattern 赋值的时候左边的元素不能有标签,否则容易引起误解:
✅
let (a, b) = (y: 4, x: 5.0)
❌
let (x: a, y: b) = (y: 4, x: 5.0)

数字、字符串字面量

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

// 这样显式的表明了类型是 Int32.
let x2: Int32 = 50
let x3 = 50 as Int32

// 如果其他上下文约束,y1 会被推断为 String.
let y1 = "a"

// 这样显式的表明了类型是 Character.
let y2: Character = "a"
let y3 = "a" as Character

// 这样显式的表明了类型是 UnicodeScalar.
let y4: UnicodeScalar = "a"
let y5 = "a" as UnicodeScalar

func writeByte(_ byte: UInt8) {
  // ...
}
// 这里的 50 会被推断为 UInt8
writeByte(50)
由此也可能引发一些隐藏的 bug。如果是直接字面量赋值,在赋值的时候编译器就会坚持类型匹配。
// 报错: 整型字面量 '9223372036854775808' 转为 'Int64' 会溢出 
let a = 0x8000_0000_0000_0000 as Int64

// 报错: 'String' 类型不能强制转为 'Character'
let b = "ab" as Character
如果使用初始化的方式则会有难以察觉的 bug:
❌
// 这里会先创建一个整型,然后再转为 UInt64。这个长度在 UInt64 是够的,但是在 Int 里会溢出
let a1 = UInt64(0x8000_0000_0000_0000)

// 这里调用的是 `Character.init(_: String)` 初始化函数,因此 "a" 被先转为了
// 字符串类型,这样浪费了性能。
let b = Character("a")

// 和上面类似,先转换成了字符串类型,在运行时才会报错
let c = Character("ab")

Playground Literal

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

自定义操作符

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