原文首发于我的blog:https://chengwey.com
Chapter 5 Optionals
本文是《Functional Programming in Swift》中第五章的笔记,如果你感兴趣,请购买英文原版。
<h2 id='CaseStudy'>1. Case Study: Dictionaries</h2>
在 swift 中 dictionary 和 array 对象一样都通过泛型实现的。下面创建一个城市的 dictionary 对象
let cities = ["Paris": 2243, "Madrid": 3216, "Amsterdam": 881, "Berlin": 3397]
这个城市字典类型是 [String: Int],我们可以使用 key 来索引他的 value:
let madridPopulation: Int = cities["Madrid"]
但是这样没有进行类型检查,很有可能 key 对应的 value 根本不存在于字典之中,于是我们可以用 optional 类型
let madridPopulation: Int? = cities["Madrid"]
当然在使用之前,我们需要用可选绑定来解包
if let madridPopulation = cities["Madrid"] {
println("The population of Madrid is " + "\(madridPopulation * 1000)")
} else {
println("Unknown city: Madrid")
}
一般是推荐采取这种可选绑定的方法进行强制解包,这种强制拆包还能让我们主动去做异常处理,水果官方的文档里提到的 implicitly unwrapped optionals 书里说这是种 a bad code smell 不过在实际工程中,像 IBOutlet 等属性用这种隐式解包还是挺有用的。
swift 还提供了一种安全使用 ! 的方式,就是当值为 nil 的时候,你要为他提供一个默认值,粗略的说一下:
infix operator ??
func ??<T>(optional: T?, defaultValue: T) -> T {
if let x = optional {
return x
} else {
return defaultValue
}
}
这是 swift 中的一个操作符, ?? 表示左边的可选类型解包成功,就输出解包后的值,否则输出右边的默认值。
但是这里有一个问题,swift 这种语言废除了 C 的预处理,他的函数会在传入参数时对参数进行求值,特别是参数是一个表达式的时候。设想一下如果 defaultValue 是一个需要大量计算的表达式,使用这种方式,无论 optional 拆包结果如何,在传参的过程中 defaultValue 都会进行求值,这无疑不是我们想要的,因为我们期待的结果仅仅是在他拆包失败的时候再计算。
解决方法也很简单,我们可以改变 defaultValue 的类型:()-> T,使其成为一个闭包,并且在参数为 void 的情况下才会被执行。
func ??<T>(optional: T?, defaultValue: () -> T) -> T {
if let x = optional {
return x
} else {
return defaultValue()
}
}
这种方式定义的操作符当然调用的时候需要创建一个闭包:
myOptional ?? { myDefaultValue }
为避免每次都要手动创建个闭包,swift基本库提供了更好的解决方案 autoclosure type attribute,这个东东其实隐式封装了所有需要创建闭包的情况,换句话说就是会为我们自动创建闭包,我们重新用 @autoclosure 来实现 ??:
infix operator ?? { associativity right precedence 110 }
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
if let x = optional {
return x
} else {
return defaultValue()
}
}
<h2 id='CombiningOptionalValues'>2. CombiningOptionalValues</h2>
Optional Chaining
swift 的 optional values 就像一个黑箱子,在打开他之前,谁也不知道里面有值否,如果多个 optional value 存在依赖关系,那么处理起来就比较复杂。
struct Order {
let orderNumber: Int
let person: Person? // ...
}
struct Person {
let name: String
let address: Address? // ...
}
struct Address {
let streetName: String let city: String
let state: String?
// ...
}
给你一个订单,我们如何找到消费者的州地址(state),使用隐式解包的方案:
order.person!.address!.state!
但如果其中有数据缺失,我们很有可能会得到一个运行时错误,下面使用可选绑定或许会安全一些:
if let myPerson = order.person in {
if let myAddress = myPerson.address in {
if let myState = myAddress.state in {
// ...
但是太啰嗦了不是,使用可选链 或许会好一些(optional chaining)
if let myState = order.person?.address?.state? {
print("This order will be shipped to \(myState\)")
} else {
print("Unknown person, address, or state.")
}
Maps and More
操作符 ? 可以让我们选择可选值的“方法” 和“属性”,还有一些例子我们可以通过函数来操控可选值:
func incrementOptional(optional: Int?) -> Int? {
if let x = optional {
return x + 1
} else {
return nil
}
}
incrementOptional 的行为类似于操作符 ?,如果可选值解包失败返回 nil,成功则执行计算。我们可以将 incrementOptional 和 ? 结合起来泛型化,定义一个 map 函数:
func map<T, U>(optional: T?, f: T -> U) -> U? {
if let x = optional {
return f(x)
} else {
return nil
}
}
map 函数带两个参数:一个可选类型 T?,一个函数类型 T -> U,如果可选类型 T? 解包成功才会进行计算,否则返回 nil 。map 函数其实也是 Swift 标准库里的一部分。使用map 我们来写 incrementOptional 函数如下:
func incrementOptional2(optional: Int?) -> Int? {
return optional.map { x in x + 1 }
}
我们也可以在我们的项目中的 optional struct 和 optional class 上应用 map 方法。为什么这个函数要叫做 map,他和数组的 map 有什么联系,这个问题放在第 14 章再讨论 :)
Optional Binding Revisited
map 方法展示了一种操作可选值的方式,但还有其他方式,思考下面的例子:
let x: Int? = 3
let y: Int? = nil
let z: Int? = x + y
编译器会报错,因为加法操作只能用于非可选值上面。为了解决这个问题,我们介绍过嵌套 if 语句的方式:
func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? {
if let x = optionalX {
if let y = optionalY {
return x + y
}
}
return nil
}
再看个例子,我们有一个字典(参照上一章的例子),包含每一个国家和对应的首都,假如指定一个国家,需要返回对应首都的人口,我们同样是通过遍历字典并对执行下面的函数,所有的国家都需要进入函数体内的层层嵌套中。
func populationOfCapital(country: String) -> Int? {
if let capital = capitals[country] {
if let population = cities[capital] {
return population * 1000
}
}
return nil
}
这样层层嵌套 if 确实很无趣,还好 Swfit 这门语言是的 function 是一等公民,我们可以自定义一个操作符来解决嵌套的问题:
infix operator >>= {}
func >>=<U, T>(optional: T?, f: T -> U?) -> U? {
if let x = optional {
return f(x)
} else {
return nil
}
}
操作符 >>= 对可选值进行判定,如果是非 nil,则将其作为参数传入函数 f 中,如何可选值为 nil,则最终结果返回 nil。使用这个操作符,我们来重写上面的例子:
func addOptionals2(optionalX: Int?, optionalY: Int?) -> Int? {
return optionalX >>= { x in
optionalY >>= { y in x+y
}
}
}
func populationOfCapital2(country: String) -> Int? {
return capitals[country] >>= { capital in
cities[capital] >>= { population in
return population * 1000
}
}
}
3. WhyOptionals?
对于用惯 Objective-C 的人来说,会觉得可选类型很古怪,但 Swift 类型系统会相当精准的:当你使用 optional 类型,就意味着可能要处理 nil。在 OC 中写出上面的例子会相当便利,不需要提前判断非空类型,编译器也不会给出警告:
- (int)populationOfCapital:(NSString *)country {
return [self.cities[self.capitals[country]] intValue] * 1000;
}
当然如果中间某步骤为 nil 的话,最终结果是 0.0,所有一切也很好。在很多没有可选类型的语言中,空指针是一切危险的源头。但在 OC 中这一切少的多,你可以安全的给 nil 发送消息,根据返回类型,你可以得到 nil,0,类似于 0 的值。但为什么要在 swift 中改变这一切呢?
选择明确可选类型的一个理由就是增加语言的静态安全性,一个强类型系统能够在 code 真正运行之前就抓取到错误,而一个明确的可选类型能够保护你因为缺失某个值而产生不可预知的崩溃。
像 OC 这种语言他也有自身的缺点,如果你使用一个字典的 key 返回 nil,这个时候需要判断是 key 不存在返回 nil,还是 key 存在与字典中,但是对应的值是 nil,为了区分这一点,只能使用 NSNull 。
虽然在 OC 中能够很安全地给 nil 发送消息,但并不意味着能够安全地使用他们。我们创建一个属性字符串,如果将传递的 country 参数设为 nil,那么 capital 也会是 nil,但是 NSAttributedString 初始化的时候使用这个为 nil 的 country 参数就会崩溃掉。
- (NSAttributedString *)attributedCapital:(NSString *)country {
NSString *capital = self.capitals[country];
NSDictionary *attributes = @{ /* ... */ };
return [[NSAttributedString alloc] initWithString:capital attributes:attributes];
}
虽然这种 crash 不会太频繁,但很多开发者的程序都会出现这种 crash,花在 debug 上的时间也不少,聪明的程序员通常会使用 asserts 来做验证:
- (NSAttributedString *)attributedCapital:(NSString *)country {
NSParameterAssert(country);
NSString *capital = self.capitals[country];
NSDictionary *attributes = @{ /* ... */ };
return [[NSAttributedString alloc] initWithString:capital attributes:attributes];
}
上面我们如果传入的 country 为 nil,预言会立即 fails 掉方便我们调试。但如果我们传递的 contry 和 capitals 字典不匹配怎么办,capitals 字典并没有我们的 key,字符串 capital 就会为 nil,同样最终程序会崩溃掉。当然要修这种错误也非常简单,但我们这里想说的是使用 Swift 处理这种 nil 引起的 Crash 要简单的多,用 Swift 也更容易写出健壮性的代码。
最后,使用这些 assertions 是天然非模块化的,假设我们实现了一个 checkCountry 方法来检查这些非空字符串是否存在:
- (NSAttributedString *)attributedCapital:(NSString*)country {
NSParameterAssert(country);
if (checkCountry(country)) {
// ...
}
}
现在疑问又来了,是否 checkCountry 函数内部实现也需要一个 assert 来判断参数是否为 nil 呢?一方面是不需要,我们已经在 attributedCapital 方法中检查过了;另一方面是需要,如果 checkCountry 函数仅支持 非 nil 参数,我们应该拷贝一份 assertion 到他内部去。综上两个矛盾的结论,我们面临两难抉择,要么冒着使用不安全的接口,要么多复制一个 assertion 。
而在 Swift 中一切变的更加简单,可选类型意味着明确了这个值可能会是 nil,这是相当宝贵的信息,像下面的函数声明:
func attributedCapital(country: String) -> NSAttributedString?
从这个函数名中我们不但能明确的知道返回值有可能会失败(nil),而且清楚传入的参数 country 一定是非 nil 的,编译器同样会利用这些信息。
还有一种情况,如果我们处理标量时,可选类型在 OC 中会变得更加狡猾,举个例子:我们尝试在一个字符串中查找一个特定的关键词,正常情况下如果 rangeOfString: 没有找到匹配的字符串,则结果应返回 NSNotFound,而 NSNotFound 的定义是 -1。这段代码几乎总是能正常的工作,但是有一个隐含的 bug 且很难排查:当 rangeOfString:消息的接收者为 nil 的话,将会返回一个充满 0 的结构体,并且 location 被设为 0,因为 0 不等于 NSNotFound 代表的 -1,最终结果会始终成立。
NSString *someString = ...;
//// someString为nil的时候执行rangeOfString方法返回range = location=0,
if ([someString rangeOfString:@"swift"].location != NSNotFound]) {
NSLog(@"Someone mentioned swift!");
}
然而使用可选类型,这一切都不会发生。上述代码在 Swift 中将会被编译器直接拒绝,因为类型系统不允许 rangeOfString: 方法运行在可选类型之上,你需要解包后才能使用该方法。
if let someString = ... {
if someString.rangeOfString("swift").location != NSNotFound {
println("Found")
}
}
类型系统和编译器会在开发阶段就帮助你减少错误,写出更健壮的代码。