这章的内容都围绕这几个准则:避免多余,避免歧义,倾向于与简洁的隐式声明,除非是会显式的可以提高可读性、减少引发歧义的可能性。
编译器警告
如果作者可以避免编译器的警告,应该消除编译器警告。
一个常见的例外是被废弃的 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、DictionaryArray<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 {
// ...
}
这样的设计也迫使调用者考虑如何处理可能发生的错误:
把调用包在
do-catch
里,决定如何处理错误。把函数声明标记为
throws
,把错误再往外抛。如果可以忽略产生的错误使用
try?
。
常规情况禁止使用try!
,这等于调用了fatalError
但是没有提供任何有效的信息。只有在单元测试的代码里可以使用。
强制解包和强制类型映射
这两种都是非常不推荐使用的方式,大多数时候都代表着不好的编程习惯。除非是周围的代码可以明显的看出这样操作是安全的,并且应该添加注释说明为什么是安全的。当然在单元测试了例外。
Implicitly Unwrapped Optionals
IUO 也是天生的不安全,所以应该尽量避免。一些几种情况例外。
UI 对象对应的
@IBOutlet
属性(@IBOutlet var button: UIButton!
),使用 IUO 可以降低每次使用时都需要考虑 optional 的情况。因为在载入 UI 时这些属性会被系统初始化,只是不在 swift 的初始化时发生。与 OC 桥接时某些缺少正确声明
nullability
的属性。单元测试。
访问权限
省略默认的访问权限( 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 里,禁止在生产环境中使用。
自定义操作符
自定义缺少共同认知的操作符会显著的降低代码可读性。应该尽量避免自定义操作符,除非这个操作符针对特定问题有着广泛的认可。比如定义两个两个矩阵结构体的 *
就很合理。