Swift学习笔记(九)--可选类型链与错误处理

可选链和错误处理在WWDC的视频里都有说, 有兴趣的可以翻出来看看. 貌似是在Advanced Swift里面说的.

可选类型链(Optional Chaining)

翻译为可选类型链感觉很奇怪, 但是一时半会又找不到更贴切的词语了, 这是Swift让我觉得很实用很方便的一点. 简单说来就是一个实例是可选类型的, 它的属性(或方法返回值)也是可选类型的, 它属性的属性还是可选的(可以一直链下去)...这种情况如果按之前提到的, 就要一直if let, 持续好几次. 这个可选类型链就是为了解决这一类问题.

同时, 我们已经知道在Swift里面给nil发消息, 那么会导致crash, 但是用可选类型链就可以规避这个问题. 所以就有了下一节这个标题.

可选类型链就强制拆包的一种替代(Optional Chaining as an Alternative to Forced Unwrapping)

如果一个对象的类型是可选的, 按之前提到的我们在使用之前都要用感叹号(!)进行强制拆包, 而可选类型链在处理可能为nil的对象的时候, 要优雅很多, 它会在对象为nil的时候返回nil(这个时候是不是很像ObjC里面的机制了呢?), 所以, 我们在使用的时候要记住一点, 如果用了可选类型链来赋值或者获得返回值, 那么这个值会是可选类型的(也就是原本返回Int, 用了可选类型链就会变成返回Int?, 因为有可能会返回nil啊).
值得一提的是, 即使原本返回Void的方法, 也会变成Void?. 同时, 如果本身就是可选类型的, 那么也不会再嵌套一层可选类型, 比如为Int??类型(可选类型是可以嵌套的, 可以看看唐巧公众号上分享的那篇文章).

我们直接看一段代码:

var str :String?
var length:Int? = str?.characters.count // 如果改为var length:Int 则会报错

官方文档上也给出了一个例子来对比强制拆包和可选类型链的区别, 一起看看:

class Person {
    var residence: Residence?
}
 
class Residence {
    var numberOfRooms = 1
}
let john = Person()
let roomCount = john.residence!.numberOfRooms  // 这里有runtime error
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")  // 执行这一行
}

// 给residence赋值
john.residence = Residence()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")  // 打印这一行
} else {
    print("Unable to retrieve the number of rooms.")
}

可选类型链的更多用法

这一节我觉得讲的和上两节差不多, 但是官网给出了很多例子, 看看代码即可. 或者可以直接看最后的那一块真正讲链式的地方, 那里才是精华, 足够让人兴奋(如果你不够兴奋, 说明你没有经历过1.0时代).

直接以官网的例子来看, 首先定义一大堆的类:

class Person {
    var residence: Residence?  // 可选类型
}

class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    func printNumberOfRooms() {
        print("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?  // 可选类型
}

class Room {
    let name: String
    init(name: String) { self.name = name }
}

class Address {
    var buildingName: String?  // 可选类型
    var buildingNumber: String?  // 可选类型
    var street: String?  // 可选类型
    func buildingIdentifier() -> String? {   // 可选类型
        if buildingName != nil {
            return buildingName
        } else if buildingNumber != nil && street != nil {
            return "\(buildingNumber) \(street)"
        } else {
            return nil
        }
    }
}

可以看到, Person的residence为可选, Residence的address为可选, Address的大量属性和方法返回值为可选, 所以, 如果我们要有Person实例, 想要通过Residence实例来访问Address里的属性, 势必要经过多次检查.

一. 首先来看用可选类型链来访问属性的情况:

let john = Person()
// 读取数据
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")  // 打印这一行
}
// 写入数据
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress  // 然而并没有赋值成功, 因为residence为nil, 直接被返回了

二. 再来看看用可选类型链调用方法的情况:
之前稍稍提过了一点, 就是即使是返回为Void的方法, 如果用可选类型链来调用, 也会变成Void?, 所以, 判断一个返回Void的方法调用成功与否可以这样:

if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")  // 打印这一行
}

同样的道理, 如果你想看赋值是否成功, 也可以类似的处理:

if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")  // 打印这一行
}

三. 用可选类型链访问下标
这一节其实就是一个规定, 这个规定也很符合常理, 就是访问可选类型链中一个对象的下标的时候, 问号(?)要加在[]之前, 如:

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.") // 打印这一行
}

这很容易理解, 毕竟我们需要先判断这个实例是否为nil, 才能进行下一步的操作.
继续看下面的代码:

john.residence?[0] = Room(name: "Bathroom") // 并不会赋值成功
// 这回给上residence
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
 
if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).") // 执行这一行
} else {
    print("Unable to retrieve the first room name.")
}

再来看看对可选类型的访问下标的操作:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0]++
testScores["Brian"]?[0] = 72
// 结果为:
// "Dave" array = [91, 82, 84] 
// "Bev"   array = [80, 94, 81]

// 多套一层可以这样:
var dict : Dictionary>?
dict = {"Ryan":[1,2,3], "Chris":[4,5,6]}
dict?["Ryan"]?[0] = 0
dict?["Chris"]?[2] = 10
// 结果为:
// "Ryan"为0,2,3
// "Chris"为4,5,10

多层链接

看了上面的例子可能没有太多的感受, 最后也还是要用if let, 感觉没有太明显的优势. 这是因为我们还没有讲到链, 链这个字就意味着我们可以持续写下去, 只要得到的值是可选类型, 我们就能链上来.

还是用上面的那些类和实例, 来感受一下链式的代码:

// 因为之前赋值的address并没有成功, 所以此时还是空的
if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.") // 执行这一行
}

// 赋上address
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
 
if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")  // 执行这一行
} else {
    print("Unable to retrieve the address.")
}

我们可以看到, 在多层次的可选类型链中, 只需要一个if let就能取出最终的返回值来进行判断, 可以省略很多的中间步骤, 直达我们的目的地, 最后以官网用链式获得返回值的例子结尾:

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// 打印 "John's building identifier is The Larches."

if let beginsWithThe =
    john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
        if beginsWithThe {
            print("John's building identifier begins with \"The\".")
        } else {
            print("John's building identifier does not begin with \"The\".")
        }
}
// 打印 "John's building identifier begins with "The"."

至此, 可选类型链就已经结束, 相信我们都会在以后的代码中频繁使用这一机制, 毕竟它能够让我们的代码更安全, 更简洁. 具体细节还是惯例查看官方文档

错误处理(Error Handling)

不管是什么语言, 只要是认真开发一个作品都是需要面对错误处理的. 之前稍稍提过一点, Swift和ObjC不一样, Swift的标准错误处理靠抛出错误(嗯, 就是错误, 毕竟它的protocol叫ErrorType而不是ExceptionType), 而不是靠返回值或者参数. 同时之前也提过一点Swift是面向协议编程的, 所以, 如果要抛出错误的话, 那么就要让抛出的对象实现ErrorType协议.

其实只要你愿意的话, 继续依赖返回值来控制错误也是可以的, 因为我们有了元组(tuple)这个利器. 至于为什么用throws来抛出错误, 主要还是让接口的调用方能够很明确这个接口可能出现的错误是什么.

官方给出一个自动售货机买小吃的例子, 可能出现的错误包括: 不存在该食品, 钱不够和库存不足.

错误的展示和抛出(Representing and Throwing Errors)

如官网所说, ErrorType只是一个空的协议, 其主要作用还是一个指示性的作用, 说明这里可能会出现错误. 同时, 官方比较推荐的做法是用枚举来指示错误, 例如下面这个例子:

enum VendingMachineError: ErrorType {
    case InvalidSelection
    case InsufficientFunds(coinsNeeded: Int)
    case OutOfStock
}
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)

处理错误

上一节说了抛出错误, 那么抛出了就要处理, Swift有四种方法来处理错误:
1). 传递给函数的调用方
2). 用do-catch语句来处理
3). 把错误当做optional值来处理
4). 断言判定错误不发生.
下面的小节会分别讲述这4种处理方法. 特别讲一下自己抛出或者处理异常的时候,
还要用到try(或者try?, try!), 这个稍后会细讲.

用可抛出错误函数来传递错误(Propagating Errors Using Throwing Functions)

翻译起来很奇怪, 其实就是说一个函数抛出错误说明它不处理这个错误, 也就要传递出去, 让调用方来处理. 如果函数没有声明为可抛出, 那么所有内部的错误都要自己处理掉.
声明的语法如下:

func canThrowErrors() throws -> String
 
func cannotThrowErrors() -> String

先看一个抛出异常的例子:

struct Item {
    var price: Int
    var count: Int
}
 
class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    func dispenseSnack(snack: String) {
        print("Dispensing \(snack)")
    }
    
    func vend(itemNamed name: String) throws {
        guard var item = inventory[name] else {
            throw VendingMachineError.InvalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.OutOfStock
        }
        
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        coinsDeposited -= item.price
        --item.count
        inventory[name] = item
        dispenseSnack(name)
    }
}

如上面的代码所示, vend(itemNamed:) 这个方法会根据传入的参数抛出3种错误(这里的guard的优势体现的很明显, 可以试试用if let来写会是什么情况...), 所以我们调用这个函数大概就是这个德行:

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {  // throws说明继续抛出
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)  
}

这里用try而不是throw, 因为throw后面要接具体的异常, 而后面的方法并不是一定返回异常的, 所以用try.

用do-catch处理异常

如果要自己消化掉错误, 就要用到do-catch(涉及到具体的语句还是得try). 先看下具体的语法吧:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
}

注意, 这里try和别的地方不一样, 只能try一条语句, 而不可以用try{}来包围起来. 官方给出下面的例子, 在我的Xcode7.2上会报枚举没有穷尽的问题, 但是实际上已经穷尽了, 不明白是什么问题, 为了继续走下去我加上了一个分支:

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
    print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.") // 触发这个error
} catch { // 兜底的catch
    print("UNKNOW ERROR")
}

错误转换为可选值(Converting Errors to Optional Values)

之前提过, try还有两种变种, try?和try!. 如果用try?来处理错误的话, 会把错误转换为一个可选值. 所以, 如果在执行try?语句的时候抛出了错误, 那么语句执行的结果就是nil(即使原本没有返回值也会返回nil, 这个之前的章节讲过). try!则是不向调用方抛出错误了. 这个下一小节讲.

先来看官方的例子:

func someThrowingFunction() throws -> Int {
    // ...
}
 
let x = try? someThrowingFunction()  // x的类型为Int?, 而不是Int
 
let y: Int?  // 常量不会在声明的时候自动置为nil, 纠正一下在构造器里的错误描述
do {
    y = try someThrowingFunction()
} catch {
    y = nil  
}

官方文档来给出了一种别的用法:

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}
去除错误传递(Disabling Error Propagation)

如果你知道一个声明为可抛出错误的函数或方法, 实际上不会抛出错误, 就会用到这种处理方法. 所以如果你判断失误, 就会有runtime error了.

官方给出的例子是需要从给定路径加载图片, 如果加载失败就抛出错误. 有的时候, 你很确定这张图片是肯定存在的, 比如随应用一起下载的图片, 在这种情况就比较适合去除错误传递了.

let photo = try! loadImage("./Resources/John Appleseed.jpg")

执行清理行为(Specifying Cleanup Actions)

在处理错误的时候可能要关闭一些资源, 例如关闭掉打开的文件(又是这个例子). 有时候可能代码写多了就会忘记写, 导致出现一些问题, 所以Swift里面引入了一个叫defer(延迟)的关键字, 这个关键字可以在代码块要结束的时候执行(所谓代码块简单来说就是用大括号包起来的代码). 所以, 不管是抛出错误, 还是return, break, 一旦要离开就开始执行defer的代码.
注意: 如果多个defer在一起, 我这边测试情况是, 先执行后面的, 再执行前面的, 并不是按照编码的顺序. 因此, 可以推断Swift应该是把需要defer的代码push到栈里面, 之后一个个执行. 至于为什么要这样, 我猜测了几种可能性都不能很好地解释, 估计是某些情况打开多个资源, 子元件又互相有依赖, 那么先释放掉最后创建的肯定是相对安全的, 因为创建资源1的时候还没有资源2, 说明资源1,2共存是可以接受的, 但是只有2没有1则不一定了.

另外需要说明的是, defer里面不能出现break, return或者抛出异常.

说了这么多, 还是先来看看例子吧:

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // Work with the file.
        }
        // close(file) is called here, at the end of the scope.
    }
}
// 我自己写了一个更加简明的例子:
var x = 0
var y = 0

if (1 < 2){
    x = 1
    y = 2
    defer{
        print("\(x)+\(y)=\(x+y)")
    }
    defer{
        print("\(2*x)+\(2*y)=\(2*x+2*y)")
    }
}

x = 3
y = 4
// 结果输出:
2+4=6
1+2=3

错误处理到这边差不多了, 比较推荐去看下WWDC上的相关视频, 细节还是参考官方文档

2016/03/09补充内容:
关于错误处理有这么个情况: 假如你写一个函数, 函数接受一个闭包, 并执行它, 如果这个闭包会抛出异常, 那么负责抛出呢, 如果是闭包抛出, 那么此函数的调用方怎么知道这个函数是否抛出异常, 抛出什么异常? 甚至再加一个条件, 如果这个函数异步执行这个闭包呢?
第一个问题的答案是, 谁出错谁抛出, 但是, 函数要加一个rethrows,
例如:

enum NumberError:ErrorType {
    case ExceededInt32Max
}

func functionWithCallback(callback:(Int) throws -> Int) rethrows {
    try callback(Int(Int32.max)+1)
}

do {
 try functionWithCallback({v in 
    if v <= Int(Int32.max) { 
        return v 
    }; 
    throw NumberError.ExceededInt32Max
  })
}
catch NumberError.ExceededInt32Max {
    "Error: exceeds Int32 maximum"
}
catch {
}

至于第二种情况, 参考这篇文章, 里面还涉及到了Promise, Result和Monad, 完全理解有一点难度.

你可能感兴趣的:(Swift学习笔记(九)--可选类型链与错误处理)