Swift中的Optional详解
对各种值为"空"的情况处理不当,几乎是所有Bug的来源。
在其它编程语言里,空值的表达方式多种多样:"" / nil / NULL / 0 / nullptr 都是我们似曾相识的表达空值的方法。
而当我们访问一个变量时,我们有太多情况无法意识到一个变量有可能为空,进而最终在程序中埋藏了一个个闪退的隐患。
因此,Swift里,明确区分了"变量"和"值有可能为空的变量"这两种情况,以时刻警告你:"哦,它的值有可能为空,我应该谨慎处理它。
而对于后者,谨慎不仅仅是精神层面的,Swift还从语法层面上,帮助你在处理空值时,游刃有余。
NSString *tmp = nil;
if ([tmp rangeOfString: @"Swift"].location != NSNotFound) {
// Will print out for nil string
NSLog(@"Something about swift");
}
在我们的例子里,尽管tmp
的值是nil
,但调用tmp
的rangeOfString
方法却是合法的,它会返回一个值为0的NSRange
,因此,location
的值也是0。
但是,NSNotFound
的值却是NSIntegerMax
。于是,尽管tmp
的值为ni
l,我们还可以在控制台看到_Something about swift_
这样的输出。
那么Swift
中是怎么解决的呢?
Swift的方法,通过把不同的结果放在一个enum里,
Swift可以通过编译器,强制我们明确处理函数返回的异常情况。
Optional关键实现技术模拟
让编译器强制我们处理可能发生错误的情况。为了做到这点,我们得满足下面这几个条件:
1、 首先,作为一个函数的返回值,它仍旧得是一个独立的类型;
2、 其次,对于所有成功的情况,这个类型得有办法包含正确的结果;
3、 最后,对于所有错误的情况,这个类型得有办法用一个和正确情况类型不同的值来表达;
4、 做到这些,当我们把一个错误情况的值用在正常的业务逻辑之后,编译器就可以由于类型错误,给我们予以警告了。
让编译器强制你处理错误的情况
说到这,我们应该就有思路了,一个包含两个case的enum正是解决这个问题的完美方案:
enum Optional<T> {
case some(T) //对于所有成功的情况,我们用case some,并且把成功的结果保存在associated value里;
case none 对于所有错误的情况,我们用case none来表示;
}
然后,我们可以给Array添加一个和std::find类似的方法:
extension Array where Element: Equatable {
func find(_ element: Element) -> Optional<Index> {
var index = startIndex
while index != endIndex {
if self[index] == element {
return .some(index)
}
formIndex(after: &index)
}
return .none
}
}
在find
的实现里,它有两个退出函数的路径。当在Array
中找到参数时,就把对应的Index
作为.some
的associated value
并返回;否则,当while
循环结束时,就返回.none
。这样,当我们用find
查找元素位置时:
var numbers = [1, 2, 3]
let index = numbers.find(4)
print(type(of: index)) // Optinal<Int>
index
的类型就会变成Optional<Int>
,于是,当我们尝试把这个类型传递给remove(at:)
时:
numbers.remove(at: index) // !!! Compile time error !!!
就会直接看到一个编译器错误:
为了使用index
中的值,我们只能这样:
switch index {
case .some(let index):
numbers.remove(at: index)
case .none:
print("Not exist")
}
看到了么?只要会发生错误的函数返回Optional
,编译器就会强制我们对调用成功和失败的情况明确分开处理。并且,当你看到一个函数返回了Optional
,从它的签名就可以知道,Hmmm,调用它有可能会发生错误,我得小心处理。
实际上,你并不需要自己定义这样的Optional
类型,Swift
中的optional
变量就是如此实现的,因此,当让find
直接返回一个Index optional
时:
func find(_ element: Element) -> Index? {
// ...
}
理解Swift
对optional
类型进行的简化处理
Optional
作为Swift
中最重要的语言特性之一,为了避免让你每次都通过.some
和.none
来处理不同的情况(毕竟,这是optional
的实现细节),Swift
在语法层面对这个类型做了诸多改进。
首先,optional
包含的.some
关联值会在必要的时候,被自动升级成optional
;而nil
字面值则会被转换成.none
。因此,我们之前的find
可以被实现成这样:
func find(_ element: Element) -> Index? {
var index = startIndex
while index != endIndex {
if self[index] == element {
return index // Simplified for .some(index)
}
formIndex(after: &index)
}
return nil // Simplified for .none
}
注意find
中的两个return
语句,你就能理解从字面值自动升级到optional
的含义了。实际上Array你也无需自己实现这样的find
,Array
中自带了一个index(of:)
方法,它的功能和实现方式,和find
是一样的。
其次,在switch
中使用optional 可选值类型?
值的时候,我们也不用明确使用.some
和.none
,Swift
同样做了类似的简化:
switch index {
case let index?:
numbers.remove(at: index)
case nil:
print("Not exist")
}
我们可以用case let index
?这样的形式来简化读取.some
的关联值,用case nil
来简化case .none
。
有哪些常用的optional
使用范式
if let
如果我们要表达“当optional
不等于nil
时,则执行某些操作”这样的语义,最朴素的写法,是这样的:
let number: Int? = 1
if number != nil {
print(number!)
}
其中,number!
这样的写法叫做force unwrapping
,用于强行读取optional
变量中的值,此时,如果optional
的值为nil
就会触发运行时错误。所以,通常,我们会事先判断optional
的值是否为nil
。
但这样写有一个弊端,如果我们需要在if代码块中包含多个访问number
的语句,就要在每一处使用number!
,这显得很啰嗦。我们明知此时number
的值不为nil
,应该可以直接使用它的值才对。为此,Swift
提供了if let
的方式,像这样:
if let number = number {
print(number)
}
在上面的代码里,我们使用if let
直接在if
代码块内部,定义了一个新的变量number
,它的值是之前number?
的值。然后,我们就可以在if
代码块内部,直接通过新定义的number来访问之前number?
的值了。
这里用了一个小技巧,就是在if let
后面新定义变量的名字,和之前的optional
是一样的。这不仅让代码看上去就像是访问optional
自身一样,而且,通常为一个optional
的值另取一个新的名字,也着实没什么必要。
除了可以直接在if let
中绑定optional
的value
,我们还可以通过布尔表达式进一步约束optional
的值,这也是一个常见的用法,例如,我们希望number为奇数:
if let number = number, number % 2 != 0 {
print(number)
}
我们之前讲到过逗号操作符在if
中的用法,在这里,number % 2 != 0
中的number
,指的是在if
代码块中新定义的变量,理解了这点,上面的代码就不存在任何问题了。
有了optional
的这种用法之后,对于那些需要一连串有可能失败的行为都成功时才执行的动作,只要这些行为都返回optional
,我们就有了一种非常漂亮的解决方法。
例如,为了从某个url加载一张jpg的图片,我们可以这样:
if let url = URL(string: imageUrl), url.pathExtension == "jpg",
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
let view = UIImageView(image: image)
}
在上面的例子里,从生成URL
对象,到根据url
创建Data
,到用data
创建一个UIImage
,每一步的继续都依赖于前一步的成功,而每一步调用的方法又都返回一个optional
,因此,通过串联多个if let,我们就把每一步成功的结果绑定在了一个新的变量上并传递给下一步,这样,比我们在每一步不断的去判断optional
是否为nil
简单多了。
while let
除了在条件分支中使用let
绑定optional
,我们也可以在循环中,使用类似的形式。例如,为了遍历一个数组,我们可以这样:
let numbers = [1, 2, 3, 4, 5, 6]
var iterator = numbers.makeIterator()
while let element = iterator.next() {
print(element)
}
在这里,iterator.next()
会返回一个Optional<Int>
,直到数组的最后一个元素遍历完之后,会返回nil
。然后,我们用while let
绑定了数组中的每一个值,并把它们打印在了控制台上。
看到这里,你可能会想,直接用一个for...in...
数组不就好了么?为什么要使用这种看上去有点儿麻烦的while
呢?
实际上,通过这个例子,我们要说明一个重要的问题:在Swift里,for...in
循环是通过while
模拟出来的,这也就意味着,for
循环中的循环变量在每次迭代的时候,都是一个全新的对象,而不是对上一个循环变量的修改:
for element in numbers {
print(element)
}
在上面这个for...in
循环里,每一次迭代,element
都是一个全新的对象,而不是在循环开始创建了一个element
之后,不断去修改它的值。用while
的例子去理解,每一次for
循环迭代中的element
,就是一个新的while let
绑定。
然而,为什么要这样做呢?
因为这样的形式,可以弥补由于closure
捕获变量带来的一个不算是bug
,却也有违直觉的问题。首先,我们来看一段JavaScript代码:
var fnArray = [];
for (var i in [0, 1, 2]) {
fnArray[i] = () => { console.log(i); };
}
fnArray[0](); // 2
fnArray[1](); // 2
fnArray[2](); // 2
对于末尾的三个fnArray
调用,你期望会返回什么结果呢?我们在每一次for...in
循环中,定义了一个打印循环变量i的箭头函数。当它们执行的时候,也许你会不假思索的脱口而出:当然是输出0, 1, 2啊。
但实际上,由于循环变量i自始至终都是同一个变量,在最后调用fnArray
中保存的每一个函数时,它们在真正执行时访问的,也都是同一个变量i。因此,这三个调用打印出来的值,都是2。类似这样的问题,稍不注意,就会在代码中,埋下Bug的隐患。
因此,在Swift
的for
循环里,每一次循环变量都是一个“新绑定”的结果,这样,无论任何时间调用这个clousre
,都不会出现类似JavaScript中的问题了。
我们把之前的那个例子,用Swift
重写一下:
var fnArray: [()->()] = []
for i in 0...2 {
fnArray.append({ print(i) })
}
fnArray[0]() // 0
fnArray[1]() // 1
fnArray[2]() // 2
这里,由于变量i在每次循环都是一个新绑定的结果,因此,每一次添加到fnArray
中的clousre
捕获到的变量都是不同的对象。当我们分别调用它们的时候,就可以得到捕获到它们的时候,各自的值了。
使用guard
简化optional unwrapping
通常情况下,我们只能在optional
被unwrapping
的作用域内,来访问它的值。
理解optional unwrapping
的作用域
例如,在下面这个arrayProcess
函数里:
func arrayProcess(array: [Int]) {
if let first = array.first {
print(first)
}
}
我们只能在if
代码块内部,访问被unwrapping
之后的值。但这样做有一个麻烦,就是如果我们要在函数内部的多个地方使用array.first
,就要在每个地方都进行某种形式的unwrapping
,这不仅写起来很麻烦,还会让代码看上去非常凌乱。
实际上,面对这种在多处访问同一个optional
的情况,更多的时候,我们需要的是一个确保optional
一定不为nil
的环境。如果,我们能在一个地方统一处理optioanl
为nil
的情况,就可以在这个地方之外,安全的访问optional
的值了。
好在,Swift
在语法上,对这个操作进行了支持,这就是guard
的用法:
func arrayProcess(array: [Int]) {
guard let first = array.first else {
return
}
print(first)
}
在上面的例子里,我们使用guard let
绑定了array.first
的非nil
值。如果array.first
为nil
,就会转而执行else
代码块里的内容。这样,我们就可以在else
内部,统一处理array.first
为nil
的情况。在这里,我们可以编写任意多行语句,唯一的要求,就是else
的最后一行必须离开当前作用域,对于函数来说,就是从函数返回,或者调用fatalError
表示一个运行时错误。
而这,也是为数不多的,我们可以在value binding
作用域外部,来访问optional value
的情况。
一个特殊情况
在Swift
里,有一类特殊的函数,它们返回Never
,表示这类方法直到程序执行结束都不会返回。Swift
管这种类型叫做uninhabited type
。
什么情况会使用Never
呢?其实并不多,一种是崩溃前,例如,使用fatalError
返回一些用于排错的消息;另一种,是类似dispatchMain
这样,在进程生命周期中一直需要执行的方法。
当我们在返回Never
的函数中,使用guard
时,else
语句并不需要离开当前作用域,而是最后一行必须调用另外一个返回Never
的函数就好了。例如下面的例子:
func toDo(item: String?) -> Never {
guard let item = item else {
fatalError("Nothing to do")
}
fatalError("Implement \(item) later")
}
在toDo
的实现里,如果我们没有指定要完成的内容,就在else
里调用fatalError
显示一个错误。在这里,fatalError
也是一个返回Never
的函数。
一个伪装的optional
除了使用真正的optional
变量之外,有时,我们还是利用编译器对optional
的识别机制来为变量的访问创造一个安全的使用环境。例如,为了把数组中第一个元素转换为String
,我们可以这样:
func arrayProcess(array: [Int]) -> String? {
let firstNumber: Int
if let first = array.first {
firstNumber = first
} else {
return nil
}
// `firstNumber` could be used here safely
return String(firstNumber)
}
在上面的代码里,有两点值得说明:
首先,我们使用了Swift
中延迟初始化的方式,在if let
中,才初始化常量firstNumber
;
其次,从程序的执行路径分析,对于firstNumber
来说,要不我们已经在if let
中完成了初始化;要不,我们已经从else
返回。因此,只要程序的执行逻辑来到了if...else...
之后,访问firstNumber
就一定是安全的了。
实际上,Swift
编译器也可以识别这样的执行逻辑。firstNumber
就像一个伪装的optional
一样,在if let
分支里被初始化成具体的值,在else
分支里,被认为值是nil
。因此,在else
代码块之后,就像在之前guard
语句之后一样,我们也可以认为firstNumber
一定是包含值的,因此安全的访问它。
通常,当我们要调用一个包含在optional
中的对象的方法时,我们可能会像下面这样把两种情况分开处理:
var swift: String? = "Swift"
let SWIFT: String
if let swift = swift {
SWIFT = swift.uppercased()
}
else {
fatalError("Cannot uppercase a nil")
}
但是,当我们仅仅想获得一个包含结果的optional类型时,上面的写法就显得有点儿啰嗦了。实际上,我们有更简单的用法:
let SWIFT = swift?.uppercased() // Optional("SWIFT")
这样,我们就会得到一个新的Optional
。并且,我们还可以把optional
对象的方法调用串联起来:
let SWIFT = swift?.uppercased().lowercased()
// Optional("swift")
上面的形式,在Swift
里,就叫做optional chaining
。只要前一个方法返回optiona
l类型,我们就可以一直把调用串联下去。但是,如果你仔细观察上面的串联方法,却可以发现一个有趣的细节:对于第一个optional
,我们调用uppercased()
方法使用的是?.
操作符,并得到了一个新的Optional
,然后,当我们继续串联lowercased()
的时候,却直接使用了.
操作符,而没有继续使用swift?.uppercased()?.lowercased()
这样的形式,这说明什么呢?
这也就是说,optional
在串联
的时候,可以对前面方法返回的optional
进行unwrapping
,如果结果非nil
就继续调用,否则就返回nil
。
但是……
这也有个特殊情况,就是如果调用的方法自身也返回一个optional
(注意:作为调用方法自身,是指的诸如uppercased()
这样的方法,而不是整个swift?.uppercased()
表达式),那么你必须老老实实在每一个串联的方法前面使用?.
操作符,来看下面这个例子。我们自己给String
添加一对toUppercased / toLowercased
方法,只不过,它们都返回一个String?
,当String为空字符串
时,它们返回nil
:
extension String {
func toUppercase() -> String? {
guard self.isEmpty != 0 else {
return nil
}
return self.uppercased()
}
func toLowercase() -> String? {
guard self.characters.count != 0 else {
return nil
}
return self.lowercased()
}
}
然后,还是之前optional chaining
的例子,这次,我们只能这样写:
let SWIFT1 = swift?.toUppercase()?.toLowercase()
注意到第二个?.
了么,由于前面的toUppercase()
返回了一个Optional
,我们只能用?.
来连接多个调用。而之前的uppercased()
则返回了一个String
,我们就可以直接使用.来串联多个方法了。
除此之外,一种不太明显的optional chaining
用法,就是用来访问Dictionary中某个Value
的方法,因为[]操作符本身也是通过函数实现的,它既然返回一个optional
,我们当然也可以chaining
:
let numbers = ["fibo6": [0, 1, 1, 2, 3, 5]]
numbers["fibo6"]?[0] // 0
因此,绝大多数时候,如果你只需要在optional不为nil
时执行某些动作,optional chaining
可以让你的代码简单的多,当然,如果你还了解了在chaining中执行的unwrapping
语义,就能在更多场景里,灵活的使用这个功能。
Nil coalescing
除了optional chaining
之外,Swift
还为optional
提供了另外一种语法上的便捷。如果我们希望在optional的值为nil
时设定一个默认值,该怎么做呢?可能你马上就会想起Swift中的三元操作符:
var userInput: String? = nil
let username = userInput != nil ? userInput! : "Mars"
但就像你看到的,?:
操作符用在optional
上的时候显得有些啰嗦,除此之外,为了实现同样的逻辑,你还无法阻止一些开发者把默认的情况写在:左边:
let username = userInput == nil ? "Mars" : userInput!
如此一来,事情就不那么让人开心了,当你穿梭在不同开发者编写的代码里,这种逻辑的转换迟早会把你搞疯掉。
于是,为了表意清晰的同时,避免上面这种顺序上的随意性,Swift引入了nil coalescing
,于是,之前username
的定义可以写成这样:
let username = userInput ?? "Mars"
其中,??
就叫做nil coalescing
操作符,optional
的值必须写在左边
,nil时的默认值必须写在右边
。这样,就同时解决了美观和一致性的问题。相比之前的用法,Swift再一次从语言设计层面履行了更容易用对,更不容易用错的准则。
除了上面这种最基本的用法之外,??
也是可以串联的,我们主要在下面这些场景里,串联多个??
:
首先,当我们想找到多个optional
中,第一个不为nil的变量:
let a: String? = nil
let b: String? = nil
let c: String? = "C"
let theFirstNonNilString = a ?? b ?? c
// Optional("C")
在上面的例子里,我们没有在表达式最右边添加默认值。这在我们串联多个??时是允许的,只不过,这样的串联结果,会导致theFirstNonNilString
的类型变成Optional
,当abc都为nil
时,整个表达式的值,就是nil
。
而如果我们这样:
let theFirstNonNilString = a ?? b ?? "C"
theFirstNonNilString
的类型,就是String
了。理解了这个机制之后,我们就可以把它用在if
分支里,通过if let
绑定第一个不为nil
的optional
变量:
if let theFirstNonNilString = a ?? b ?? c {
print(theFirstNonNilString) // C
}
这样的方式,要比你在if条件分支中,写上一堆||直观和美观多了。
其次,当我们把一个双层嵌套的optional
用在nil coalescing
操作符的串联里时,要格外注意变量的评估顺序。来看下面的例子:
假设,我们有三个optional
,第一个是双层嵌套的optional
:
let one: Int?? = nil
let two: Int? = 2
let three: Int? = 3
当我们把one / two / three串联起来时,整个表达式的结果是2。这个很好理解,因为,整个表达式中,第一个非nil的optional的值是2:
one ?? two ?? three // 2
当我们把one的值修改成.some(nil)时,上面这个表达式的结果是什么呢?
let one: Int?? = .some(nil)
let two: Int? = 2
let three: Int? = 3
one ?? two ?? three // nil
此时,这个表达式的结果会是nil,为什么呢?这是因为:
评估到one
时,它的值是.some(nil)
,但是.some(nil)并不是nil
,于是它自然就被当作第一个非nil的optional变量被采纳了
;
被采纳之后,Swift
会unwrapping
这个optional
的值作为整个表达式的值,于是就得到最终nil
的结果了;
理解了这个过程之后,我们再来看下面的表达式,它的值又是多少呢?
(one ?? two) ?? three // 3
正确的答案是3。这是因为我们要先评估()内的表达式,按照刚才我们提到的规则,(one ?? two)的结果是nil,于是nil ?? three的结果,自然就是3了。
当你完全理解了双层嵌套的optional
在上面三个场景中的评估方式之后,你就明白为什么要对这种类型的串联保持高度警惕了。因为,optional
的两种值nil
和.some(nil)
,以及表达式中是否存在()改变优先级,都会影响整个表达式的评估结果。
为什么需要双层嵌套的Optional
?
如果一个optional
封装的类型又是一个optional
会怎样呢?
首先,假设我们有一个String
类型的Array
:
let stringOnes: [String] = ["1", "One"]
当我们要把stringOnes
转变成一个Int数组
的时候:
let intOnes = stringOnes.map { Int($0) }
此时,我们就会得到一个[Optional<Int>]
,当我们遍历intOnes
的时候,就可以看到这个结果:
intOnes.forEach { print($0) }
// Optional<Int>
// nil
至此,一切都没什么问题。但当你按照我们在之前提到过的while
的方式遍历intOnes
的时候,你就会发现,Swift
悄悄对嵌套的optional
进行了处理:
var i = intOnes.makeIterator()
while let i = i.next() {
print(i)
}
// Optional<Int>
// nil
虽然,这会得到和之前for
循环同样的结果。但是仔细分析while
的执行过程,你会发现,由于next()
自身返回一个optional
,而ineOnes
中元素的类型又是Optional<Int>
,因此intOnes
的迭代器指向的结果就是一个Optional<Optional<Int>>
。
当intOnes
中的元素不为nil
时,通过while let
得到的结果,就是我们看到的经过一层unwrapping之后的Optional(1)
;
当intOnes
中的元素为nil
时,我们可以看到while let
的到的结果并不是Optional(nil)
,而直接是nil
;
这说明Swift
对嵌套在optional
内部的nil
进行了识别,当遇到这类情况时,可以直接把nil
提取出来,表示结果为nil
。
了解了这个特性之后,我们就可以使用for...in
来正常遍历intOnes了。例如,使用我们之前提到的for case
来读取所有的非nil值:
for case let one? in intOnes {
print(one) // 1
}
或者统计所有的nil值:
for case nil in intOnes {
print("got a nil value")
}
如果Swift
不能对optional
中嵌套的nil
进行自动处理,上面的for
循环是无法正常工作的。
什么时候需要强制解包
我们都知道,对于一个optional
变量来说,可以用!来强行读取optional
包含的值,Swift
管它叫作force unwrapping
。然而,这种操作并不安全
,强制读取值为nil的optional会引发运行时错误
。于是,每当我们默默在一个optional后面写上!的时候,心里总是会隐隐感到一丝纠结。我们到底什么时候该使用force unwrapping呢?
无论是在Apple的官方文档,还是在Stack overflow上的各种讨论中,你都能找到类似下面的言论:
永远都不要使用这个东西,你会有更好的办法;
当你确定optional一定不为nil时;
当你确定你真的必须这样做时;
...
然而,当你没有切身体会的时候,似乎很难理解这些言论的真实含义。其实,就在我们上一节内容的最后,就已经遇到了一个非常具体的例子:
extension Sequence {
func myFlatMap<T>(_ transform:
(Iterator.Element) -> T?) -> [T] {
return self.map(transform)
.filter { $0 != nil }
.map { $0! } // Safely force unwrapping
}
}
在我们用filter { $0 != nil }
过滤掉了self
中,所有的非nil
元素之后,在map
里,我们要获得所有optional
元素中包含的值,这时,对$0
使用force unwrapping
,就满足了之前提到的两个条件:
我们可以确定此时$0一定不为nil
;
我们也确定真的必须如此;
现在,你对于“绝对安全”和“必须如此”这两个条件,应该有一个更具体的认识了。所以,但凡没有给你如此强烈安全感的场景,不要使用force unwrapping
。
而对于第一种“永远都不要使用force unwrapping”
的言论,其实也有它的道理,毕竟在我们之前对optional
的各种应用方式里,你的确几乎看不到我们使用了force unwrapping
。
甚至,即便当你身处在一个相当安全的环境里,的确相比force unwrapping
,你会有更好的方法。例如,对下面这个表示视频信息的Dictionary来说:
let episodes = [
"The fail of sentinal values": 100,
"Common optional operation": 150,
"Nested optionals": 180,
"Map and flatMap": 220,
]
Key表示视频的标题,Value表示视频的秒数。
如果,我们要对视频时长大于100秒的视频标题排序,形成一个新的Array,就可以这样:
episodes.keys
.filter { episodes[$0]! > 100 }
.sorted()
在filter
中,我们筛选大于100秒时长的视频时,这里使用force unwrapping
也是绝对安全的。因为episode
是一个普通的Dictionary,它一定不为nil,因此,我们也一定可以使用keys读到它的所有键值,即便episodes不包含任何内容也没问题。然后,既然读到了键值,用force unwrapping读取它的value,自然也是安全的了
。
所以,这也算是一个可以使用force unwrapping
的场景。但就像我们刚才说的那样,实际上,你仍有语义更好的表达方式,毕竟在filter内部再去访问episodes看上去并不那么美观。怎么做呢?
episodes.filter { (_, duration) in duration > 100 }
.map { (title, _) in title }
.sorted()
我们可以对整个Dictionary
进行筛选,首先找到所有时长大于100的视频形成新的Dictionary
,然后,把所有的标题,map
成一个普通的Array
,最后,再对它排序。这样,我们就不用任何force unwrapping
了,而且,就表意来说,要比之前的版本,容易理解的多。
两个调试optional
的小技巧
尽管前面我们提到了很多使用optional
的正确方式,以及列举了诸多不要使用force unwrapping
的理由,但现实中,你还是或多或少会跟各种使用了force unwrapping
的代码打交道。使用这些代码,就像拆弹一样,稍不留神它就会让我们的程序崩溃。因此,我们需要一些简单易行的方式,让它在跟我们翻脸前,至少留下些更有用的内容。
改进force unwrapping
的错误消息
得益于Swift可以自定义操作符的特性,一个更好的主意是我们自定义一个force unwrapping
操作符的加强版,允许我们自定义发生运行时错误的消息。既然一个!表示force unwrapping
,那我们暂且就定义一个!!操作符就好了。它用起来,像这样:
var record = ["name": "11"]
record["type"] !! "Do not have a key named type"
怎么做呢?
首先,在上面的例子里,!!是一个中序操作符(infix operator)
,也就是说,它位于两个操作数中间,我们这样来定义它:
infix operator !!
其次,我们把它定义为一个泛型函数,因为我们并不知道optional
中包含的对象类型。这个函数有两个参数,第一个参数是左操作数,表示我们要force unwrapping的optional
对象,第二个参数是右操作数
,表示我们要在访问到nil时显示的错误消息:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
// TODO: implement later
}
最后,!!<T>
的实现就很简单了,成功unwrapping
到,就返回结果,否则,就用fatalError
打印运行时错误:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
if let value = optional { return value }
fatalError(errorMsg)
}
这样,我们上面的record["type"]
就会得到下面的运行时错误:
fatal error
于是,即便发生意外,至少我们也还能够让程序“死个明白”。
进一步改进force unwrapping
的安全性
当然,除了在运行时死的明白之外,我们还可以把调试日志只留在debug mode
,并在release mode
,为force unwrapping到nil的情况
提供一个默认值。就像之前我们提到过的??
类似,我们来定义一个!?
操作符来实现这个过程:
infix operator !?
func !?<T: ExpressibleByStringLiteral>(
optional: T?,
errorMsg: @autoclosure () -> String) -> T {
assert(optional != nil, errorMsg())
return optional ?? ""
}
在上面的代码里,我们使用ExpressibleByStringLiteral
这个protocol
约束了类型T必须是一个String
,之所以要做这个约束,是因为我们要为nil
的情况提供一个默认值。
在!?
的实现里,assert
仅在debug mode
生效,它的执行的逻辑,和我们实现!!
操作符时是一样的。而在release mode
,我们直接使用了??
操作符,为String?
提供了一个空字符串默认值。
于是,当我们这样使用record["type"]
的时候:
record["type"] !? "Do not have a key named type"
我们就只会在debug mode
得到和之前同样的运行时错误,而在release mode
,则会得到一个空字符串。或者,基于这种方法,我们还可以有更灵活的选择。例如,借助Tuple
,我们同时可以自定义nil时使用的默认值和运行时错误:
func !?<T: ExpressibleByStringLiteral>(
optional: T?,
nilDefault: @autoclosure () -> (errorMsg: String, value: T)) -> T {
assert(optional != nil, nilDefault().errorMsg)
return optional ?? nilDefault().value
}
然后,我们的record["Type"]
就可以改成:
record["type"] !? ("Do not have a key named type", "Free")
这样,在release mode,record["type"]的值,就是“Free”了
。理解了这个方式的原理之后,我们就可以使用Swift
标准库中提供了Expressible
家族,来对各种类型的optional
进行约束了:
ExpressibleByNilLiteral
ExpressibleByArrayLiteral
ExpressibleByFloatLiteral
ExpressibleByStringLiteral
ExpressibleByIntegerLiteral
ExpressibleByBooleanLiteral
...
最后,我们再来看一种特殊的情况,当我们通过optional chaining
得到的结果为Void?
时,例如这样:
record["type"]?.write(" account")
由于Swift并没有提供类似ExpressibleByVoidLiteral
这样的protocol
,为了方便调试Optional<Void>
,我们只能再手动重载一个非泛型版本的!?
:
func !?(optional: Void?, errorMsg: @autoclosure () -> String) {
assert(optional != nil, errorMsg())
}
然后,就可以在debug mode
调试Optional<Void>
了:
record["type"]?
.write(" account")
!? "Do not have a key named type"