iOS - Swiftswift 语法iOS 的那些事儿

Swift中的Optional详解

2017-02-19  本文已影响509人  AKyS佐毅
WechatIMG31.jpeg

对各种值为"空"的情况处理不当,几乎是所有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,但调用tmprangeOfString方法却是合法的,它会返回一个值为0的NSRange,因此,location的值也是0。

但是,NSNotFound的值却是NSIntegerMax。于是,尽管tmp的值为nil,我们还可以在控制台看到_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作为.someassociated 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? {
    // ...
}

理解Swiftoptional类型进行的简化处理

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你也无需自己实现这样的findArray中自带了一个index(of:)方法,它的功能和实现方式,和find是一样的。

其次,在switch中使用optional 可选值类型?值的时候,我们也不用明确使用.some.noneSwift同样做了类似的简化:

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中绑定optionalvalue,我们还可以通过布尔表达式进一步约束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的隐患。

因此,在Swiftfor循环里,每一次循环变量都是一个“新绑定”的结果,这样,无论任何时间调用这个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

通常情况下,我们只能在optionalunwrapping的作用域内,来访问它的值。
理解optional unwrapping的作用域

例如,在下面这个arrayProcess函数里:

func arrayProcess(array: [Int]) {
    if let first = array.first {
        print(first)
    }
}

我们只能在if代码块内部,访问被unwrapping之后的值。但这样做有一个麻烦,就是如果我们要在函数内部的多个地方使用array.first,就要在每个地方都进行某种形式的unwrapping,这不仅写起来很麻烦,还会让代码看上去非常凌乱。

实际上,面对这种在多处访问同一个optional的情况,更多的时候,我们需要的是一个确保optional一定不为nil的环境。如果,我们能在一个地方统一处理optioanlnil的情况,就可以在这个地方之外,安全的访问optional的值了。

好在,Swift在语法上,对这个操作进行了支持,这就是guard的用法:

func arrayProcess(array: [Int]) {
    guard let first = array.first else {
        return
    }

    print(first)
}

在上面的例子里,我们使用guard let绑定了array.first的非nil值。如果array.firstnil,就会转而执行else代码块里的内容。这样,我们就可以在else内部,统一处理array.firstnil的情况。在这里,我们可以编写任意多行语句,唯一的要求,就是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。只要前一个方法返回optional类型,我们就可以一直把调用串联下去。但是,如果你仔细观察上面的串联方法,却可以发现一个有趣的细节:对于第一个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绑定第一个不为niloptional变量:

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变量被采纳了
被采纳之后,Swiftunwrapping这个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"
上一篇下一篇

猜你喜欢

热点阅读