哨岗值
在编程世界中有一种非常通用的模式,那就是某个操作是否要返回一个有效值。
当你在读取文件并读到文件末尾时,也许期望的是不返回值,就像下面的 C 代码这样:
int ch;
while ((ch = getchar()) != EOF) {
printf("Read character %c\n", ch);
}
printf("Reached end-of-file\n");
复制代码
EOF 只是对于 -1 的一个 ##define
。如果文件中还有其他字符,getchar 将会返回它们。一旦到达文件末尾,getchar 将返回 -1。
又或者返回空值意味着 “未找到”,就像下面这段 C++ 代码一样:
auto vec = {1, 2, 3};
auto iterator = std::find(vec.begin(), vec.end(), someValue);
if (iterator != vec.end()) {
std::cout << "vec contains " << *iterator << std::endl;
}
复制代码
在这里,vec.end() 返回的迭代器表示容器最后一个元素的下一个位置。这是一个特殊的迭代器,你可以用它来检查容器末尾,但是和 Swift 集合类型中的 endIndex 类似,你不能实际用它来获取一个值。find 使用它来表达容器中没有这样的值。
再或者,是因为函数处理过程中发生了某些错误,而导致没有值能被返回。其中,最臭名昭著的例子大概就是空指针了。下面这句看起来人畜无害的 Java 代码就将抛出一个 NullPointerException
:
int i = Integer.getInteger("123")
复制代码
因为实际上 Integer.getInteger
做的事情并不是将字符串解析为整数,它实际上会去尝试获取一个叫做 “123” 的系统属性的整数值。因为系统中并不存在这样的属性,所以 getInteger 返回的是 null。当 null 被拆箱成一个 int 时,Java 将抛出异常。
这里还有一个 Objective-C 的例子:
[[NSString alloc] initWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error];
复制代码
这个初始化方法有可能返回 nil,只有在这种情况下,你才应该去检查 error 指针。如果返回的是非 nil 的话,error 并不一定是个有效的指针。
在上面所有例子中,这些函数都返回了一个 “魔法” 数来表示其并没有返回真实的值。这样的值被称为 “哨岗值 (sentinel values)”。
不过这种策略是有问题的。因为返回的结果不管从哪个角度看都很像一个真实值。-1 也是一个有效的整数,但你却不想将它打印出来。v.end() 也是一个迭代器,但你读取这个位置的值时,结果却是未定义的。另外,当你的 Java 程序抛出一个 NullPointerException 时,所有人都想看的是栈转储信息 (stack dump)。
和 Java 不同,Objective-C 允许我们向 nil 发送消息。这种行为是 “安全” 的,因为 Objective-C 的运行时会保证向 nil 发送消息时,返回值总是等于 0。也就是说,根据消息的返回值是对象或数值,运行时会分别使用 nil 和 0 表示,以此类推。如果消息返回的是一个结构体,那么它的所有属性都将被初始化为零。记住这一点后,让我们来看看下面这个查找子字符串的例子:
NSString *someString = ...;
if ([someString rangeOfString:@"Swift"].location != NSNotFound) {
NSLog(@"Someone mentioned Swift!");
}
复制代码
如果 someString 是 nil,那么 rangeOfString: 消息将返回一个值都为零的 NSRange。也就是 说,.location 将为零,而 NSNotFound 被定义为 NSIntegerMax。这样一来,当 someString 是 nil 时,if 语句的内容将被执行,而其实这并不应该发生。
哨岗值的另一个问题是使用它们还需要一些前置知识。比如像是 C++ 的 end 迭代器,或 Objective-C 中错误处理这样约定俗成的用法。如果没有这些约定,或你不知道它们,那你只能 依赖文档进行开发了。另外,一个函数也没有办法来表明自己不会失败。也就是说,当一个函数返回指针时,这个指针有可能绝对不会是 nil。但是除了阅读文档之外,你并没有办法能知道这个事实。更甚者有可能文档本身就是错的。
通过枚举解决魔法数的问题
大多数语言都支持某种形式的枚举类型,用它表达某个类型可能包含的所有值是一种更为安全的做法。
Swift 更进一步,它在枚举中引入了 “关联值” 的概念。也就是说,枚举可以在它们的成员中包含另外的关联的值。 Optional 也是通过枚举实现的:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
复制代码
获取枚举关联值的唯一方法是通过模式匹配,就像在 switch
或 if case let
中使用的匹配方法一 样。和哨岗值不同,除非你显式地检查并解包,否则是不可能意外地获取到一个 Optional 中包装的值的。
因此,Swift 中与 find 等效的方法 firstIndex(of:)
返回的不是一个哨岗值,而是一个 Optional<Index>
。以下是一个类似的实现:
extension Collection where Element: Equatable {
func firstIndex(of element: Element) -> Optional<Index> {
var idx = startIndex
while idx != endIndex {
if self[idx] == element {
return .some(idx)
}
formIndex(after: &idx)
}
// 没有找到,返回 .none
return .none
}
}
复制代码
因为可选值是 Swift 中非常重要和基础的类型,所以有很多让它用起来更简洁的语法:
Optional<Index>
可以写成Index?
;可选值遵守ExpressibleByNilLiteral
协议,因此你可以用nil
替代.none
;像上面 idx 这样的非可选值将在需要的时候自动 “升级” 为 可选值,这样你就可以直接写return idx
,而不用return .some(idx)
了。这个语法糖实际上掩盖了Optional
类型的真正本质。请时刻牢记,可选值并不是什么魔法,它就是一个普通的枚举值。即便 Swift 没有提供它,你也完全可以自己定义一个。
现在,用户就不会在没有检查的情况下,错误地使用一个值了:
var array = ["one", "two", "three"]
let idx = array.firstIndex(of: "four")
// 编译错误:remove(at:) 接受 Int,而不是 Optional<Int>。 array.remove(at: idx)
复制代码
相反,假设得到的结果不是 .none,为了使用包装在可选值中的索引,你必须对其进行 “解包”:
var array = ["one","two","three"]
switch array.firstIndex(of: "four") {
case .some(let idx):
array.remove(at: idx)
case .none:
break // 什么都不做
}
复制代码
在这个 switch 语句中我们使用了匹配普通枚举的语法来处理可选值,例如,提取 .some
关联值的用法。这种做法非常安全,但是读写起来都不是很顺畅。一种更简明的写法是使用 ? 作为 在 switch 中对 some 进行匹配时的模式后缀,另外,你还可以使用 nil 字面量来匹配 none:
switch array.firstIndex(of: "four") {
case let idx?:
array.remove(at: idx)
case nil:
break // 什么都不做
}
复制代码
但是这还是有点太笨重。我们接下来会看看其他一些简短而又清晰的处理可选值的方式,你可
以根据你的使用情景酌情选择。
可选值概览
可选值在 Swift 中有很多来自语言内建的支持。
if let
使用 if let 来进行可选值绑定 (optional binding) 要比上面使用 switch 语句要稍好一些。if let 语句会检查可选值是否为 nil,如果不是 nil,便会解包可选值。idx 的类型就是 Int (而不再是可选值),并且 idx 也只在这个 if let 语句的作用域中有效:
var arr = ["One", "Two", "Three", "Four"]
if let index = arr.firstIndex(of: "Four") {
print(index) // 3
}
复制代码
你可以把布尔限定语句与 if let 搭配在一起使用。因此,如果要实现 “查找到的位置不数组的第一个元素,就xxx” 这样的逻辑,可以这样:
var arr = ["One", "Two", "Three", "Four"]
if let index = arr.firstIndex(of: "One"), index != arr.startIndex {
print(index)
} else {
print("No!")
}
// No!
复制代码
你也可以在同一个 if 语句中绑定多个值。更赞的是,在后面的绑定中可以使用之前成功解包出来的结果。当你要连续调用多个返回可选值的函数时,这个功能就特别有用了。比如,下面的 URL 和 UIImage 的构造方法都是 “可失败的 (failable)”,也就是说,要是你的 URL 是无效的,或者数据不是一个图片数据,这些方法都会返回 nil。而 Data 的初始化方法会抛出错误,我们可以通过 try? 来把它转变为一个可选值。它们三者的调用可以通过这样的方式串联起来:
let urlString = "https://www.objc.io/logo.png"
if let url = URL(string: urlString),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
let view = UIImageView(image: image)
PlaygroundPage.current.liveView = view
}
复制代码
多个 let 的任意部分也能拥有布尔值限定的语句:
if let url = URL(string: urlString), url.pathExtension == "png",
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
// ...
}
复制代码
最后,你可以在同一个 if 中将可选值绑定,布尔语句和 case let 用任意的方式组合在一起使用。
wihle let
while let 语句和 if let 非常相似,它表示当一个条件返回 nil 时便终止循环。
标准库中的 readLine 函数从标准输入中读取内容,并返回一个可选字符串。当到达输入末尾 时,这个函数将返回 nil。所以,你可以使用 while let 来实现一个非常基础的和 Unix 中 cat 命 令等价的功能:
while let line = readLine() {
print(line)
}
复制代码
和 if let 一样,你可以在可选绑定后面添加一个布尔值语句。如果你想在遇到 EOF 或者空行的时候终止循环的话,只需要加一个判断空字符串的语句就行了。要注意,一旦条件为 false,循环就会停止。
while let line = readLine(), !line.isEmpty {
print(line)
}
复制代码
for x in seq 这样的循环语句需要 seq 遵守 Sequence 协议。该协议提供 makeIterator 方法来创建迭代器,而迭代器中的 next 方法将不断返回序列中的值,并在序列中的值被耗尽的时候,返回 nil。while let 非常适合用在这个场景中:
let array = [1, 2, 3]
var iterator = array.makeIterator()
while let i = iterator.next() {
print(i, terminator: " ")
} // 1 2 3
复制代码
因为一个 for 循环其实就是 while 循环,这样一来,for 循环也支持布尔语句就是情理之中的事 了,只是,我们要在布尔语句之前,使用 where 关键字:
for i in 0..<10 where i % 2 == 0 {
print(i, terminator: " ")
} // 0 2 4 6 8
复制代码
注意上面的 where 语句和 while 循环中的布尔语句工作方式有所不同。在 while 循环中,一旦值为 false 时,迭代就将停止。而在 for 循环里,它的工作方式就和 filter 相似了。如果我们将上面的 for 循环用 while 重写的话,看起来是这样的:
var iterator2 = (0..<10).makeIterator()
while let i = iterator2.next() {
guard i % 2 == 0 else { continue }
print(i)
}
复制代码
双重可选值
说到这,是时候来看看一个可选值的包装类型也是一个可选值的情况了,这会导致可选值的嵌 套。不过,这既不是一个奇怪的边界情况,编译器也不应该自动合并嵌套的可选值类型。为了 了解这种应用场景,假设有一个表示数字的字符串数组,为了把它转换成整数数组,你可能会 使用 map 进行转换:
let stringNumbers = ["1", "2", "three"]
let maybeInts = stringNumbers.map { Int($0) } // [Optional(1), Optional(2), nil]
复制代码
你现在得到了一个元素类型为 Optional<Int>
(也就是 Int?) 的数组,这是因为 Int.init(String)
是可能失败的,只要字符串无法转换成整数。我们的例子中,最后一个元素就将是 nil,因为字符串 “three” 无法转换成一个整数。
于是,当使用 for 遍历 maybeInts 的时候,自然访问到的每个元素都是 Int? 了:
for maybeInt in maybeInts {
// maybeInt 是一个 Int? 值
// 得到两个整数值和一个 `nil`
}
复制代码
我们已经知道 for…in 是 while 循环加上一个迭代器的简写方式。由于 next 方法会把序列中的 每个元素包装成可选值,所以 iterator.next() 函数返回的其实是一个 Optional<Optional<Int>>
值,或者说是一个 Int??。而 while let 会解包并检查这个值是不是 nil,如果不是,则绑定解包 的值并运行循环体部分:
var iterator = maybeInts.makeIterator()
while let maybeInt = iterator.next() {
print(maybeInt, terminator: " ")
}
// Optional(1) Optional(2) nil
复制代码
当循环到达最后一个值,也就是从 “three” 转换而来的 nil 时,从 next 返回的其实是一个非 nil 的值,这个值是 .some(nil)。while let 将这个值解包,并将解包结果 (也就是 nil) 绑定到 maybeInt 上。如果没有嵌套可选值的话,这个操作将无法完成。
顺带一提,如果你只想对非 nil 的值做 for 循环的话,可以使用 case 来进行模式匹配:
var a = ["1", "2", "Three"]
var b = a.map( { Int($0) } )
for case let x? in b {
print(x) // 1 2
}
for case let nil in b {
print("nil") // nil
}
复制代码
这里使用了 x? 这个模式,它只会匹配那些非 nil 的值。这个语法是 .some(x) 的简写形式,所以 该循环还可以被写为: for case let .some(x) in b
基于 case 的模式匹配可以让我们把在 switch 的匹配中用到的规则同样地应用到 if,for 和 while 上去。最有用的场景是结合可选值,但是也有一些其他的使用方式,比如:
基于 case 的模式匹配可以让我们把在 switch 的匹配中用到的规则同样地应用到 if,for 和 while 上去。最有用的场景是结合可选值,但是也有一些其他的使用方式,比如:
letj=5
if case 0..<10 = j {
print("\(j) 在范围内")
} // 5 在范围内
复制代码
if var and while var
除了 let 以外,你还可以使用 var 来搭配 if、while 和 for。这让你可以在语句块中改变变量:
let number = "1"
if var i = Int(number) {
i+=1
print(i)
} // 2
复制代码
不过注意,i 会是一个本地的复制。任何对 i 的改变将不会影响到原来的可选值。可选值是值类型,解包一个可选值做的事情是将它里面的值复制出来。
解包后可选值的作用域
有时候只能在 if 块的内部访问被解包的变量确实让人感到是一种限制。举个例子,数组有个 first 方法,它会返回数组首个元素的可选值,如果数组为空的话,则返回 nil。它是下面这段代码的简写:
let array = [1,2,3]
if !array.isEmpty {
print(array[0])
}
// if 块的外部,编译器无法保证 a[0] 的有效性
复制代码
如果你使用 first 的话,你就必须先对其解包才能使用它的值。这十分安全,你不会由于粗心而
忘掉这件事情:
if let firstElement = array.first {
print(firstElement)
}
// if 块的外部,不能使用 firstElement
复制代码
解包后的值只能在 if let 代码块中使用,这在绝大多数情况下都很好。但当 if 语句的目的是在某些条件不满足时提前退出函数的话,这个特性就不太实用了。提前退出 (early exit) 可以帮助我们在这个函数稍后的部分避免嵌套或者重复的检查。例如,你可能会编写下面这样的代码:
func doStuff(withArray a: [Int]) {
if a.isEmpty {
return
}
// 现在就可以安全地使用 a[0] 或 a.first! 了
}
复制代码
这里,if let 无法继续帮助我们实现期望的功能了,因为在 if 语句块之后,绑定的值就离开它的作用域了。不过,你还是可以确定数组中至少包含一个元素,所以即使语法上看上去有些恼人,但对第一个元素强制解包还是安全的。
extension String {
var fileExt: String? {
let res: String.Index
if let idx = lastIndex(of: ".") {
res = idx
} else {
return nil
}
let extStat = index(after: res)
return String(self[extStat...])
}
}
print("readme.txt".fileExt ?? "nil") // txt
复制代码
编译器会检查并确保你的代码只有两条执行路径:一条是没有找到 “.” 就提早返回;另一条则是 period 被正确初始化。因此,在 if 语句之后,res 不可能为 nil (因为 res 就不是一个可选值),也不可能未被初始化 (Swift 不允许你使用未初始化的变量),你也就完全可以不用在代码中考虑可选值的问题了。
但是,上面这两个例子看起来都很丑。我们在这里真正需要的其实只是一个类似 if not let 的语句,而这正是 guard let 所做的事情。
func doStuff(withArray a: [Int]) {
guard let firstElement = a.first else {
return
}
// firstElement 在这里已经被解包了
}
复制代码
第二个例子也变得清晰得多了:
extension String {
var fileExt: String? {
guard let res = lastIndex(of: ".") else {
return nil
}
let start = index(after: res)
return String(self[start...])
}
}
print("readme.txt".fileExt ?? "nil") // txt
复制代码
在 guard 的 else 代码块中,你可以执行任意代码,例如,在其中包含一个完整的 if…else 语句 也没问题。它唯一的要求是必须离开当前的作用域,通常这意味着一条 return 语句,或抛出一个错误,亦或调用 fatalError (或者其他返回 Never 的方法)。如果你是在循环中使用 guard 的话,那么最后也可以是 break 或者 continue。
一个返回 Never 的函数用于通知编译器:它绝对不会返回。有两类常见的函数会这么 做:一种是像 fatalError 那样表示程序失败的函数,另一种是像 dispatchMain 那样 运行在整个程序生命周期的函数。编译器会使用这个信息来检查和确保控制流正确。举例来说,guard 语句的 else 路径必须退出当前域或者调用一个不会返回的函数。
Never 又被叫做无人类型 (uninhabited type)。这种类型没有有效值,因此也不能够被构建。在泛型环境里,把 Never 和 Result 结合在一起是非常有用的。例如,对于一个接受
Result<A, E>
(A 和 E 都是泛型参数) 的泛型 API,你可以传入一个Result<..., Never>
表示这个结果中永远都不会包含失败的情况,因为这种情况是构建不出来的。另外,一个声明为返回无人类型的函数也绝对不可能正常返回。在 Swift 中,无人类型是通过一个不包含任意成员的 enum 实现的:public enum Never {}
一般来说,你不会自己定义返回 Never 的方法,除非你在为 fatalError 或 preconditionFailure 写封装。一个很有意思的应用场景是,当你要创建一个很复杂的 switch 语句,在逐条编写每个 case 的过程中,编译器就会用空的 case 语句或者是没有返回值这样的错误一直轰炸你,而你又想先集中精力处理某一个 case 语句的逻辑。这时,放几个 fatalError() 就能让编译器闭嘴。你还可以写一个 unimplemented() 方法,这样能够更好地表达这些调用是暂时没有实现的意思:
func unimplemented() -> Never {fatalError("This code path is notimplemented yet.")}
Swift 在区分各种” 无 “类型上非常严密。除了 nil 和 Never,还有 Void,Void 是空元组 (tuple) 的另一种写法
public typealias Void = ()
Void 或者 () 最常见的用法是作为那些不返回任何东西的函数的返回值,不过它也还有其他使用场景。举例来说,在一个响应式编程的框架中,使用
Observable<T>
类型对事件流进行建模,这里 T 表示发送事件中携带的内容类型。比如文本框会提供一个Observable<String>
,在每次用户编辑文本时发送事件。类似地,按钮对象也会在用 户每次点击按钮时发送事件,不过这个事件没有附加的内容,所以它的事件流类型将会是Observable<()>
。
Swift 对 “东西不存在”(nil),“存在且为空”(Void) 以及 “不可能发生” (Never) 这几个概念进行了仔细的区分。
和可选值绑定的情况不同,单单使用 guard 并没有太多好处。实际上它还要比原来的版本稍微 啰嗦一些。不过用这种方式来提前退出还是有其可取之处的,比如有时候 (但不是像我们的这个 例子这样) 使用反向的布尔条件会让事情更清楚一些。另外,在阅读代码时,guard 是一个明确 的信号,它暗示我们 “只在条件成立的情况下继续”。最后 Swift 编译器还会检查你是否确实在
guard 块中退出了当前作用域,如果没有的话,你会得到一个编译错误。因为可以得到编译器 帮助,所以我们建议尽量选择使用 guard,即便 if。
可选链
在 Objective-C 中,对 nil 发消息什么都不会发生。Swift 里,我们可以通过 “可选链 (optional chaining)” 来达到同样的效果:delegate?.callback()
但和 Objective-C 不同的是,Swift 编译器会强制要求你声明消息的接受者可能为 nil。这里的问号对代码的读者来说是一个清晰地信号,表示方法可能会不被调用。
当你通过调用可选链得到一个返回值时,这个返回值本身也会是可选值。看看下面的代码你就知道为什么需要这么设定了:
let str: String? = "Never say never" // 我们希望 upper 是大写的字符串
let upper: String
if str != nil {
upper = str!.uppercased()
} else {
fatalError("no idea what to do now...")
}
复制代码
如果 str 不等于 nil,upper 就会有我们想要的值。但如果 str 等于 nil,upper 就没有办法设置一个值了。因此使用可选链的时候,下面的 upper2 只能是可选值,因为它需要考虑 str 可能为 nil 的情况let upper2 = str?.uppercased() // Optional("NEVER SAY NEVER")
正如同可选链名字所暗示的那样,你可以将可选值的调用链接起来let lower = str?.uppercased().lowercased() // Optional("never say never")
这看起来有点出乎意料。我们不是刚刚才说过可选链调用的结果是一个可选值么?所以为什么在 uppercased()
后面不需要加上问号呢?这是因为可选链是一个 “展平” 操作。str?.uppercased()
返回了一个可选值,如果你再对它调用 ?.lowercased()
的话,逻辑上来说你将得到一个可选值的可选值。不过其实你想要得到的是一个普通的可选值,所以我们在写链上第二个调用时不需要包含可选的问号,因为可选的特性已经在之前就被捕获了。
另一方面,如果 uppercased 方法本身也返回一个可选值的话,你就需要在它后面加上 ? 来表 示你正在链接这个可选值。比如,让我们对 Int 类型进行扩展,添加一个计算属性 half,这个属 性将把整数值除以二并返回结果。但是如果数字不够大的话,比如当数字小于 2 时,函数将返 回 nil:
extension Int {
var half: Int? {
guardself < -1||self > 1 else { return nil }
return self / 2
}
}
复制代码
因为调用 half 返回一个可选结果,因此当我们重复调用它时,需要一直添加问号。因为函数的每一步都有可能返回 nil:20.half?.half?.half // Optional(2)
编译器非常聪明,它能为我们展平结果类型。上面的表达式的类型正是我们期待的 Int?,而不 是 Int???。后一种类型可以给我们更多的信息,比如说可选链是在哪个部分解包失败的,但是 这也会让结果非常难以处理,从而让可选链一开始时给我们带来的便利性损失殆尽。
可选链对下标操作也同样适用,比如:
let dictOfArrays = ["nine": [0, 1, 2, 3]]
dictOfArrays["nine"]?[3] // Optional(3)
let dictOfFunctions: [String: (Int, Int) -> Int] = [
"add": (+),
"subtract": (-)
]
dictOfFunctions["add"]?(1, 1) // Optional(2)
复制代码
设想一个类在某个事件发生时,要通过调用存储在其中的回调函数来通知其所有者,上面的特
性就会非常有用。比如有一个 TextField 类:
class TextField {
private(set) var text = ""
var didChange: ((String) -> ())?
// 被框架调用的事件处理方法。
func textDidChange(newText: String) {
text = newText
// 如果不是 nil 的话,触发回调。
didChange?(text) }
}
复制代码
didChange 属性存储了一个回调函数,每当用户编辑文本时,这个函数都会被调用。因为文本框的所有者并不一定需要注册这个回调,所以该属性是可选值,它的初始值为 nil 。当这个回调被调用的时候 (在上面的 textDidChange 方法中),可选链的写法就非常简洁了。
你还可以通过可选链来进行赋值。假设你有一个可选值变量,如果它不是 nil 的话,你想要更新它的一个属性:
struct Person {
var name: String
var age: Int
}
var optionalLisa: Person? = Person(name: "Lisa Simpson", age: 8)
// 如果不是 nil,则增加 age
if optionalLisa != nil { optionalLisa!.age += 1 }
复制代码
这种写法非常繁琐,也很丑陋。特别注意,在这种情况下你不能使用可选绑定。因为 Person 是一个结构体,所以它是一个值类型,绑定后的值只是原来值的局部作用域的复制,对这个复制进行变更,并不会影响原来的值:
if var lisa = optionalLisa {
// 对 lisa 的变更不会改变 optionalLisa 中的属性
lisa.age += 1
}
复制代码
如果 Person 是类的话,这么做是可行的。就算能用可选绑定,这么写还是太过复杂了。其实,你可以使用可选值链来进行赋值,如果它不是 nil 的话,赋值操作将会成功:optionalLisa?.age += 1
一个有点古怪 (但逻辑上合理) 的边界情况是你完全可以像下面这样直接给 Optional 赋值:
vara:Int?=5
a ?= 10
a // Optional(10)
var b: Int? = nil
b ?= 10
b // nil
复制代码
请注意 a = 10 和 a? = 10 的细微不同。前一种写法无条件地将一个新值赋给变量,而后一种写法只在 a 的值在赋值发生前不是 nil 的时候才生效。
nil 合并运算符
很多时候,你都会想在解包可选值的同时,为 nil 的情况设置一个默认值。而这正是 nil 合并运
算符的功能:
let stringteger = "1"
let number = Int(stringteger) ?? 0
复制代码
如果字符串可以被转换为一个整数的话,number 将会是那个解包后的整数值。如果不能的话, Int.init 将返回 nil,默认值 0 会被用来赋值给 number。也就是说 lhs ?? rhs 做的事情类似于这 样的代码 lhs != nil ? lhs! : rhs。
如果你之前使用过其他语言,那么可能会觉得 nil 合并操作符非常类似于 a ? b : c 这样的三元操 作符。
因为 Swift 数组中有一个 first 属性,当数组为空时,它将为 nil。这样,你就可以直接使用 nil 合并操作符来完成这件事array.first ?? 0 // 1
这个解决方法漂亮且清晰,“从数组中获取第一个元素” 这个意图被放在最前面,之后是通过 ?? 操作符连接的使用默认值的语句,它代表 “这是一个默认值”。
当你发现你在检查某个条件来确保语句有效的时候,往往意味着使用可选值会是一个更好的选择。假设你要做的不是对空数组判定,而是要检查一个索引值是否在数组边界内:
array.count > 5 ? array[5] : 0 // 0
不像 first 和 last,通过索引值从数组中获取元素不会返回 Optional。不过我们可以对 Array 进 行扩展来包含这个功能:
extension Array {
subscript(guarded idx: Int) -> Element? {
guard (startIndex..<endIndex) .contains(idx) else { return nil }
return self[idx]
}
}
var arr = [1,2,3,4]
print(arr[guarded: 5] ?? 1) // 1
复制代码
合并操作也能够进行链接 — 如果你有多个可选值,并且想要选择第一个非 nil 的值,你可以将它们按顺序合并:
let i: Int? = nil
let j: Int? = nil
let k: Int? = 1
i ?? j ?? k ?? 0 // 1
复制代码
正因为如此,所以如果你要处理的是双重嵌套的可选值,并且想使用 ?? 操作符的话,需要特别 小心区分 a ?? b ?? c 和 (a ?? b) ?? c。前者是合并操作的链接,而后者是先解包括号内的内容, 然后再处理外层:
let s1: String?? = nil // nil
(s1 ?? "inner") ?? "outer" // inner
let s2: String?? = .some(nil) // Optional(nil)
(s2 ?? "inner") ?? "outer" // outer
复制代码
和 || 操作符一样,?? 操作符使用短路求值 (short circuiting)。当我们用 l ?? r 时,只有当 l 为 nil 时,r 的部分才会被求值。这是因为在操作符的函数声明中,对第二个参数使用了 @autoclosure。
在字符串插值中使用可选值
可能你已经注意到了,当你尝试打印一个可选值或者将一个可选值用在字符串插值表达式中时,
编译器会给出警告:
let bodyTemperature: Double? = 37.0
let bloodGlucose: Double? = nil
print(bodyTemperature) // Optional(37.0)
// 警告:表达式被隐式强制从 'Double?' 转换为 Any
print("Blood glucose level: \(bloodGlucose)") // Blood glucose level: nil // 警告:字符串插值将使用调试时的可选值描述,
// 请确认这确实是你想要做的。
复制代码
很多时候这个警告很有用,它可以防止我们把 “Optional(…)” 或者 “nil” 这样的东西不小心弄到我们想要显示给用户的文本里。你需要确保避免直接在面向用户的字符串中使用可选值,请一定记住先对它们进行解包。因为所有类型都允许放在字符串插值中 (包括了 Optional),所以编译器不能将内嵌可选值当作一个错误,给出警告是它能做出的最好的选择。
有时候你确实会想要在字符串插值中使用可选值,比如想要在调试的时候将它的值打印出来, 在这种情况下,警告就很烦人了。编译器为我们提供了几种修正这个警告的方式:显式地用 as Any 进行转换,使用 ! 对值进行强制解包 (如果你能确定该值不为 nil 时),使用 String(describing: ⋯) 对它进行包装,或者用 nil 合并运算符提供一个默认值。
最后一种做法通常是比较快捷和优雅的方式,但是它有一点不足:在 ?? 表达式两侧的类型必须匹配,也就是说,你为一个 Double? 类型提供的默认值必须是 Double。因为我们最终的目标是将表达式转换为一个字符串,所以如果我们能够一开始就提供一个字符串作为默认值的话,就会特别方便。
Swift 的 ?? 运算符不支持这种类型不匹配的操作,确实,它无法决定当表达式两侧不共享同样 的基础类型时,到底应该使用哪一个类型。不过,只是为了在字符串插值中使用可选值这一特 殊目的的话,添加一个我们自己的运算符也很简单。让我们把它叫做 ???:
infix operator ???: NilCoalescingPrecedence
public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String
{
switch optional {
case let value?: return String(describing: value)
case nil: return defaultValue()
}
}
复制代码
这个函数接受左侧的可选值 T? 和右侧的字符串。如果可选值不是 nil,我们将它解包,然后返 回它的字符串描述。否则,我们将传入的默认字符串返回。@autoclosure 标注确保了只有当需 要的时候,我们才会对第二个表达式进行求值。
可选值 map
假设,我们要把一个字符数组的第一个元素转换成字符串:
let characters: [Character] = ["a", "b", "c"]
String(characters[0]) // a
复制代码
如果 characters 可能为空的话,我们可以用 if let,在数组不为空的时候创建字符串:
var firstCharAsString: String? = nil
if let char = characters.first {
firstCharAsString = String(char)
}
复制代码
这样一来,firstCharAsString 就只在数组不为空的时候才包含对应元素的字符串,否则,它就是 nil。
这种只在可选值不为 nil 的时候才进行转换的模式十分常见。因此,可选值提供了一个 map 方法专门处理这个问题。它接受一个转换可选值内容的函数作为参数。把刚才转换字符数组的功能用 map 来实现,就是这样的:
let firstChar = characters.first.map { String($0) } // Optional("a")
复制代码
显然,这个 map 和数组以及其他序列里的 map 方法非常类似。但是与序列中操作一系列值所不同的是,可选值的 map 方法只会操作一个值,那就是该可选值中的那个可能存在的值。你可以把可选值当作一个包含零个或者一个值的集合,这样 map 要么在零个值的情况下不做处理, 要么在有值的时候会对其进行转换。
extension Optional {
func myMap<U>(transform: (Wrapped) -> U) -> U? {
guard let value = self else {
return nil
}
return transform(value)
}
}
复制代码
当你想要的就是一个可选值结果时,Optional.map 就非常有用。假设你要为数组实现一个变种的 reduce 方法,这个方法不接受初始值,而是直接使用数组中的首个元素作为初始值 (在一些 语言中,这个函数可能被叫做 reduce1,但是 Swift 里我们有重载,所以也将它叫做 reduce 就行了)。
extension Array {
func myReduce(_ nextPartialResult: (Element, Element) -> Element) -> Element? {
guard let x = first else {
return nil
}
return dropFirst().reduce(x, nextPartialResult)
}
}
var a = [1,2,3,4,5]
print(a.myReduce(+)!) // 15
复制代码
因为可选值为 nil 时,可选值 map 也会返回 nil,所以我们可以不使用 guard,而就用一个
return 来重写我们的这个 reduce:
extension Array {
func myReduce(_ nextPartialResult: (Element, Element) -> Element) -> Element? {
return first.map { dropFirst().reduce($0, nextPartialResult) }
}
}
var a = [1,2,3,4,5]
print(a.myReduce(+)!) // 15
复制代码
可选值 flatMap
在集合上运行 map 并给定一个变换函数可以获取新的集合,但是一般来说我们想要的结果会是一个单一的数组,而不是数组的数组。
类似地,如果你对一个可选值调用 map,但是你的转换函数本身也返回可选值的话,最终结果 将是一个双重嵌套的可选值。举个例子,比如你想要获取数组的第一个字符串元素,并将它转换为数字。首先你使用数组上的 first,然后用 map 将它转换为数字:
let stringNumbers = ["1", "2", "3", "foo"]
let x = stringNumbers.first.map { Int($0) } // Optional(Optional(1))
复制代码
问题在于,map 返回可选值 (因为 first 可能会是 nil),并且 Int(String) 也返回可选值 (字符串可能不是一个整数),最后 x 的结果将会是 Int??。
flatMap 可以把结果展平为单个可选值。这样一来,y 的类型将会是 Int?:
let y = stringNumbers.first.flatMap { Int($0) } // Optional(1)
复制代码
flatMap 和 if let 是非常相似的。在本章早些时候,我们已经看过
使用多个 if-let 语句的例子了。它们也可以用 map 和 flatMap 实现:
let urlString = "https://www.objc.io/logo.png"
let view = URL(string: urlString)
.flatMap { try? Data(contentsOf: $0) }
.flatMap { UIImage(data: $0) }
.map { UIImageView(image: $0) }
if let view = view{ PlaygroundPage.current.liveView = view }
复制代码
可选链也和 flatMap 很相似:i?.advance(by: 1) 实际上和 i.flatMap { $0.advance(by: 1) } 是等价的。
我们已经看到多个 if-let 语句等价于 flatMap,所以我们可以用这种方式来实现它:至此,就像我们已经看到的,既然多个 if let 语句连接在一起的用法和 flatMap 是等价的,我们就可以像下面这样,用前者来实现后者:
extension Optional {
func flatMap<U>(transform: (Wrapped) -> U?) -> U? {
if let value = self, let transformed = transform(value) {
return transformed
}
return nil
}
}
复制代码
使用 compactMap 过滤 nil
如果你的序列中包含可选值,可能你会只对那些非 nil 值感兴趣。实际上,你可能只想忽略掉它
们。
设想在一个字符串数组中你只想处理数字。在 for 循环中,用可选值模式匹配可以很简单地就实现:
var arr = ["1", "2", "3", "a"]
var sum = 0
for case let i? in arr.map({ Int($0) }) {
sum += i
}
print(sum) // 6
复制代码
你可能也会想用 ?? 来把 nil 替换成 0:
var arr = ["1", "2", "3", "a"]
print(arr.map({ Int($0) }).reduce(0, { $0 + ($1 ?? 0) })) // 6
复制代码
实际上,你想要的版本应该是一个可以将那些 nil 过滤出去并将非 nil 值进行解包的 map。标准库中序列的 compactMap 正是你想要的:
print(arr.compactMap({ Int($0) }).reduce(0, +)) // 6
复制代码
extension Sequence {
func myCompactMap<B>(_ transform: (Element) -> B?) -> [B] {
return lazy.map(transform).filter { $0 != nil }.map { $0! }
}
}
复制代码
在这个实现中,使用了 lazy 来将数组的实际创建推迟到了使用前的最后一刻。这可能只是一个 小的优化,但在处理较大的序列时还是值得的。使用 lazy 可以避免多个作为中间结果的数组的 内存分配。
可选值判等
通常,你不关心一个值是不是 nil,只想检查它是否包含某个特定值而已:
let regex = "^Hello$" // ...
if regex.first == "^" {
// 只匹配字符串开头
}
复制代码
在这种情况下,值是否是 nil 并不关键。如果字符串是空,它的第一个字符肯定不是插入符号 ^,所以你不会进入到相应的代码块中。但是你肯定还是会想要 first 为你带来的安全保障和简 洁。如果用替代的写法,将会是 if !regex.isEmpty && regex[regex.startIndex] == "^"
,这太 可怕了。
上面的代码之所以能工作主要基于两点。首先,只有当 Wrapped 类型实现了 Equatable 协议,那么 Optional 才会也实现 Equatable 协议:
extension Optional: Equatable where Wrapped: Equatable {
static func ==(l: Wrapped?, r: Wrapped?) -> Bool {
switch (l, r) {
case (nil, nil): return true
case let (x?, y?): return x == y
case (_, nil), (nil, _): return false
}
}
}
复制代码
当比较两个可选值时,会有四种组合的可能性:两者都是 nil;两者都有值;两者中有一个有值,另一个是 nil。switch 语句完成了对这四种组合的遍历,所以这里并不需要 default 语句。两个 nil 的情况被定义为相等,而 nil 永远不可能等于非 nil,两个非 nil 的值将通过解包后的值是否相等来进行判断。
注意一下,我们并不一定要写这样的代码:
if regex.first == Optional("^") { // 或者: == .some("^")
// 只匹配字符串开头
}
复制代码
这是因为当你在使用一个非可选值的时候,如果需要匹配成可选值类型,Swift 总是会将它 “升级” 为一个可选值。
这个隐式的转换对于写出清晰紧凑的代码特别有帮助。设想要是没有这样的转换,但是还是希 望调用者在使用的时候比较容易的话,你需要 == 可以同时作用于可选值和非可选值。这样一来,会需要实现三个分开的版本:
//两者都可选
func == <T: Equatable>(lhs: T?, rhs: T?) -> Bool
// lhs 非可选
func == <T: Equatable>(lhs: T, rhs: T?) -> Bool
// rhs 非可选
func == <T: Equatable>(lhs: T?, rhs: T) -> Bool
复制代码
不过事实是我们只需要第一个版本,编译器会帮助我们将值在需要时转变为可选值。
实际上,我们都依赖这个隐式的转换。比方说,当我们在实现可选 map 时,我们将 内部的实际值进行转换并返回。但是我们知道 map 的返回值其实是个可选值。编译器自动帮我 们完成了转换,得益于此,我们不需要写return Optional(transform(value))
这样的代码。
Swift 代码也一直依赖这个隐式转换。例如,使用键作为下标在字典中查找时,因为键有可能不 存在,所以返回值是可选值。对于用下标读取和写入时,所需要的类型是相同的。也就是说, 在使用下标进行赋值时,我们其实需要传入一个可选值。如果没有隐式转换,你就必须写像是myDict["someKey"] = Optional(someValue)
这样的代码。
附带提一句,如果你想知道当使用下标操作为字典的某个键赋值 nil 会发生什么的话,答案就是这个键会从字典中移除。有时候这会很有用,但是这也意味着你在使用字典来存储可选值类型时需要小心一些。看看这个字典:
var dictWithNils: [String: Int?] = [
"one": 1,
"two": 2,
"none": nil
]
复制代码
这个字典有三个键,其中一个的值是 nil。如果我们想要把 “two” 的键也设置为 nil 的话,下面的代码是做不到的:
dictWithNils["two"] = nil
dictWithNils // ["one": Optional(1), "none": nil]
复制代码
它将会把 “two” 这个键移除。为了改变这个键的值,你可以使用下面中的任意一个。它们都可以正常工作,所以选择一个你觉得清晰的就可:
dictWithNils["two"] = Optional(nil)
dictWithNils["two"] = .some(nil)
dictWithNils["two"]? = nil
dictWithNils // ["one": Optional(1), "none": nil, "two": nil]
复制代码
注意上面的第三个版本和其他两个稍有不同。它之所以能够工作,是因为 “two” 这个键已经存 在于字典中了,所以它使用了可选链的方式来在获取成功后对值进行设置。现在来看看对于不 存在的键进行设置会怎么样:
dictWithNils["three"]? = nil
dictWithNils.index(forKey: "three") // nil
复制代码
你可以看到并没有值被更新或者插入。
可选值比较
和 == 类似,可选值曾经也是拥有 <、>、<= 和 >= 这些操作符的。在 Swift 3.0 中,这些操作符 从可选值中被移除了,因为它们容易导致意外的结果。
比如说,nil < .some(_)
会返回 true。在与高阶函数或者可选绑定结合起来使用的时候,会产生 很多意外。考虑下面的例子:
let temps = ["-459.67", "98.6", "0", "warm"]
let belowFreezing = temps.filter { Double($0) < 0 }
复制代码
因为Double("warm")
将会返回 nil,而 nil 是小于 0 的,所以它将被包含在 belowFreezing 温 度中,这显然是不合情理的。
如果你想要在可选值之间进行除了相等之外的关系比较的话,现在你需要先对它们进行解包, 然后明确地指出 nil 要如何处理。
强制解包的时机
上面提到的例子都用了很干净的方式来解包可选值,那什么时候你应该用感叹号 (!) 这个强制解 包运算符呢?在网上散布着各种说法,比如 “绝不使用”,“当可以让代码变清晰时使用”,或者 “在不可避免的时候使用”。我们提出了下面这个规则,它概括了大多数的场景:
当你能确定你的某个值不可能是 nil 时可以使用叹号,你应当会希望如果它意外是 nil 的话,程序应当直接挂掉。
extension Sequence {
func compactMap<B>(_ transform: (Element) -> B?) -> [B] {
return lazy.map(transform).filter { $0 != nil }.map { $0! } }
}
复制代码
在这里,因为 filter 的阶段已经把所有 nil 元素过滤出去了,所以 map 的时候没有任何可能会出现 $0! 碰到 nil 的情况。当然可以把强制解包运算符从这个函数中消除掉,通过使用循环的方法一个一个检查数组中的元素,并将那些非 nil 的元素添加到一个数组中。但是 filter / map 结 合起来的版本更加简洁和清晰,所以这里使用 ! 是完全没有问题的。
let ages = ["Tim": 53,"Angela":54,"Craig":44, "Jony": 47, "Chris": 37, "Michael": 34]
ages.keys
.filter { name in ages[name]! < 50 }
.sorted()
// ["Chris", "Craig", "Jony", "Michael"]
复制代码
同样,这里使用 ! 是非常安全的 — 因为所有的键都来源于字典,所以在字典中找不到这个键是 不可能的。
不过你也可以通过一些手段重写这几句代码,来把强制解包移除掉。利用字典本身是一个键值对的序列这一特性,你可以对序列先进行过滤,然后再通过映射来返回键的序列:
ages.filter { (_, age) in age < 50 }
.map { (name, _) in name }
.sorted()
// ["Chris", "Craig", "Jony", "Michael"]
复制代码
这样的写法可能还会额外带来一些性能上的收益,它避免了不必要的用键进行的查找。
尽管如此,有时还是会造化弄人,你有一个可选值,并且确实知道它不可能是 nil。在这种情况 下,当你碰到一个 nil 值的时候,你肯定会选择让程序挂掉而不是让它继续运行,因为这意味在 你的逻辑中有一个非常严重的 bug。此时,终止程序而不是让它继续运行会是更好的抉择,这里
! 这一个符号就实现了 “解包” 和 “报错” 两种功能的结合。相比于使用 nil 可选链或者合并运算 符来在背后清除这种理论上不可能存在的情况的方法,直接强制解包的处理方式通常要好一些。
改进强制解包的错误信息
就算你要对一个可选值进行强制解包,除了使用 ! 操作符以外,你还有其他的选择。当程序发生错误时,你从输出的 log 中无法通过描述知道原因是什么。
其实,你可能会留一个注释来提醒为什么这里要使用强制解包。那为什么不把这个注释直接作 为错误信息呢?这里我们加了一个 !! 操作符,它将强制解包和一个更具有描述性质的错误信息 结合在一起,当程序意外退出时,这个信息也会被打印出来:
infix operator !!
func !! <T>(wrapped: T?, failureText: @autoclosure () -> String) -> T {
if let x = wrapped { return x }
fatalError(failureText())
}
复制代码
现在你可以写出更能描述问题的错误信息了,它还包括了你期望的被解包的值:
let s = "foo"
let i = Int(s) !! "Expecting integer, got \"\(s)\""
复制代码
在调试版本中进行断言
说实话,选择在发布版中让应用崩溃还是很大胆的行为。通常,你可能会选择在调试版本或者
测试版本中进行断言,让程序崩溃,但是在最终产品中,你可能会把它替换成像是零或者空数
组这样的默认值。
我们可以实现一个疑问感叹号 !? 操作符来代表这个行为。我们将这个操作符定义为对失败的解 包进行断言,并且在断言不触发的发布版本中将值替换为默认值:
infix operator !?
func !?<T: ExpressibleByIntegerLiteral>
(wrapped: T?, failureText: @autoclosure () -> String) -> T
{
assert(wrapped != nil, failureText())
return wrapped ?? 0
}
复制代码
现在,下面的代码将在调试时触发断言,但在发布版本中打印 0:
lets="20"
let i = Int(s) !? "Expecting integer, got \"\(s)\""
复制代码
对其他字面量转换协议进行重载,可以覆盖不少能够有默认值的类型:
func !?<T: ExpressibleByArrayLiteral>
(wrapped: T?, failureText: @autoclosure () -> String) -> T
{
assert(wrapped != nil, failureText())
return wrapped ?? []
}
func !?<T: ExpressibleByStringLiteral>
(wrapped: T?, failureText: @autoclosure () -> String) -> T
{
assert(wrapped != nil, failureText())
return wrapped ?? ""
}
复制代码
如果你想要显式地提供一个不同的默认值,或者是为非标准的类型提供这个操作符,我们可以
定义一个接受元组为参数的版本,元组包含默认值和错误信息:
func !?<T>(wrapped: T?,
nilDefault: @autoclosure () -> (value: T, text: String)) -> T
{
assert(wrapped != nil, nilDefault().text)
return wrapped ?? nilDefault().value
}
// 调试版本中断言,发布版本中返回 5 Int(s)
!? (5, "Expected integer")
复制代码
因为对于返回 Void 的函数,使用可选链进行调用时将返回 Void?,所以利用这一点,你也可以写一个非泛型的版本来检测一个可选链调用碰到 nil,且无操作的情况:
func !?(wrapped: ()?, failureText: @autoclosure () -> String) {
assert(wrapped != nil, failureText())
}
var output: String? = nil
output?.write("something") !? "Wasn't expecting chained nil here"
复制代码
想要挂起一个操作我们有三种方式。首先,fatalError 将接受一条信息,并且无条件 地停止操作。第二种选择,使用 assert 来检查条件,当条件结果为 false 时,停止执 行并输出信息。在发布版本中,assert 会被移除掉,也就是说条件不会被检测,操作
也永远不会挂起。第三种方式是使用 precondition,它和 assert 有一样的接口,但是 在发布版本中不会被移除,也就是说,只要条件被判定为 false,执行就会被停止。
隐式解包可选值
像 UIView! 这种在类型的后面加一个感叹号的隐式解包可选值,尽管不论何时你使用它们的时候都会自动强制解包,但它们仍然只是可选值。现在我们已经知道了当可选值是 nil 的时候强制解包会造成应用崩溃,那你到底为什么会要用到隐式可选值呢?实际上有两个原因:
原因 1:暂时来说,你可能还需要到 Objective-C 里去调用那些没有检查返回是否存在的代码; 或者你会调用一个没有针对 Swift 做注解的 C 语言的库。
隐式解包可选值还存在的唯一原因其实是为了能更容易地和 Objective-C 与 C 一起使用。在早期我们刚开始通过 Swift 来使用已经存在的那些 Objective-C 代码时,所有返回引用的 Objective-C 方法都被转换为了返回一个隐式的可选值。因为其实 Objective-C 中表示引用是否可以为空的语法是最近才被引入的,以前除了假设返回的引用可能是 nil 引用以外,也没有什么好办法。但是只有很少的 Objective-C API 会真的返回一个空引用,所以将它们自动在 Swift 里暴露为普通的可选值是一件很烦人的事情。因为所有人都已经习惯了 Objective-C 世界中对象 “可能为空” 的设定,因此把这样的返回值作为隐式解包可选值来使用是可以说得过去的。
所以你会在有些没有经过检查的 Objective-C 桥接代码中看到它们的身影。但你不应该看见一个纯的原生 Swift API 返回一个隐式可选值,也不应该把它们传入回调中。
原因 2:因为一个值只是很短暂地为 nil,在一段时间后,它就再也不会是 nil。
最常见的情况就是两阶段初始化 (two-phase initialization)。当你的类准备好被使用时,所有 的隐式解包可选值都将有一个值。这就是 Xcode 和 Interface Builder 在 view controller 的生 命周期中使用它们的方式:在 Cocoa 和 Cocoa Touch 中,view controller 会延时创建他们的 view,所以在 view controller 自身已经被初始化,但是它的 view 还没有被加载的这段时间窗口内,view 的对象的 outlet 引用还没有被创建。
隐式可选值行为
虽然隐式解包的可选值在行为上就好像是非可选值一样,不过你依然可以对它们使用可选链, nil 合并,if let,map 或者将它们与 nil 比较,所有的这些操作都是一样的:
var s: String! = "Hello"
s?.isEmpty // Optional(false)
if let s = s { print(s) } // Hello
s=nil
s ?? "Goodbye" // Goodbye
复制代码
回顾
可选值是 Swift 的一大卖点,它是让开发者得以书写更安全的代码的最大特性之一,而且我们 完全同意这个说法。但仔细想想,其实真正带来变化的不是可选值,而是非可选值。几乎所有 的主流语言都有类似 “null” 或者 “nil” 的概念;它们中的大多数所缺乏的是把一个值声明为 “从 不为 nil” 的能力。或者,反过来想,有一些类型 (比如 Objective-C 或 Java 中的非 class 类型) “总是不为 nil”,这又让开发者们必须用某个魔法数来表示缺少一个值的情况。
设计 API 的过程中,根据实际需要让输入和输出包含精心设计过的可选值,不仅会让调用函数 的代码更具表现力,这些函数用起来也会更简单。因为通过签名可以传递更多的信息,开发者 也就不用总是诉诸文档了。