Swift學(xué)習(xí)筆記(九)--可選類型鏈與錯(cuò)誤處理

可選鏈和錯(cuò)誤處理在WWDC的視頻里都有說, 有興趣的可以翻出來看看. 貌似是在Advanced Swift里面說的.

可選類型鏈(Optional Chaining)

翻譯為可選類型鏈感覺很奇怪, 但是一時(shí)半會(huì)又找不到更貼切的詞語了, 這是Swift讓我覺得很實(shí)用很方便的一點(diǎn). 簡單說來就是一個(gè)實(shí)例是可選類型的, 它的屬性(或方法返回值)也是可選類型的, 它屬性的屬性還是可選的(可以一直鏈下去)...這種情況如果按之前提到的, 就要一直if let, 持續(xù)好幾次. 這個(gè)可選類型鏈就是為了解決這一類問題.

同時(shí), 我們已經(jīng)知道在Swift里面給nil發(fā)消息, 那么會(huì)導(dǎo)致crash, 但是用可選類型鏈就可以規(guī)避這個(gè)問題. 所以就有了下一節(jié)這個(gè)標(biāo)題.

可選類型鏈就強(qiáng)制拆包的一種替代(Optional Chaining as an Alternative to Forced Unwrapping)

如果一個(gè)對象的類型是可選的, 按之前提到的我們在使用之前都要用感嘆號(hào)(!)進(jìn)行強(qiáng)制拆包, 而可選類型鏈在處理可能為nil的對象的時(shí)候, 要優(yōu)雅很多, 它會(huì)在對象為nil的時(shí)候返回nil(這個(gè)時(shí)候是不是很像ObjC里面的機(jī)制了呢?), 所以, 我們在使用的時(shí)候要記住一點(diǎn), 如果用了可選類型鏈來賦值或者獲得返回值, 那么這個(gè)值會(huì)是可選類型的(也就是原本返回Int, 用了可選類型鏈就會(huì)變成返回Int?, 因?yàn)橛锌赡軙?huì)返回nil啊).
值得一提的是, 即使原本返回Void的方法, 也會(huì)變成Void?. 同時(shí), 如果本身就是可選類型的, 那么也不會(huì)再嵌套一層可選類型, 比如為Int??類型(可選類型是可以嵌套的, 可以看看唐巧公眾號(hào)上分享的那篇文章).

我們直接看一段代碼:

var str :String?
var length:Int? = str?.characters.count // 如果改為var length:Int 則會(huì)報(bào)錯(cuò)

官方文檔上也給出了一個(gè)例子來對比強(qiáng)制拆包和可選類型鏈的區(qū)別, 一起看看:

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.")  // 執(zhí)行這一行
}

// 給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.")
}

可選類型鏈的更多用法

這一節(jié)我覺得講的和上兩節(jié)差不多, 但是官網(wǎng)給出了很多例子, 看看代碼即可. 或者可以直接看最后的那一塊真正講鏈?zhǔn)降牡胤? 那里才是精華, 足夠讓人興奮(如果你不夠興奮, 說明你沒有經(jīng)歷過1.0時(shí)代).

直接以官網(wǎng)的例子來看, 首先定義一大堆的類:

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實(shí)例, 想要通過Residence實(shí)例來訪問Address里的屬性, 勢必要經(jīng)過多次檢查.

一. 首先來看用可選類型鏈來訪問屬性的情況:

let john = Person()
// 讀取數(shù)據(jù)
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")  // 打印這一行
}
// 寫入數(shù)據(jù)
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress  // 然而并沒有賦值成功, 因?yàn)閞esidence為nil, 直接被返回了

二. 再來看看用可選類型鏈調(diào)用方法的情況:
之前稍稍提過了一點(diǎn), 就是即使是返回為Void的方法, 如果用可選類型鏈來調(diào)用, 也會(huì)變成Void?, 所以, 判斷一個(gè)返回Void的方法調(diào)用成功與否可以這樣:

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.")  // 打印這一行
}

三. 用可選類型鏈訪問下標(biāo)
這一節(jié)其實(shí)就是一個(gè)規(guī)定, 這個(gè)規(guī)定也很符合常理, 就是訪問可選類型鏈中一個(gè)對象的下標(biāo)的時(shí)候, 問號(hào)(?)要加在[]之前, 如:

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

這很容易理解, 畢竟我們需要先判斷這個(gè)實(shí)例是否為nil, 才能進(jìn)行下一步的操作.
繼續(xù)看下面的代碼:

john.residence?[0] = Room(name: "Bathroom") // 并不會(huì)賦值成功
// 這回給上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).") // 執(zhí)行這一行
} else {
    print("Unable to retrieve the first room name.")
}

再來看看對可選類型的訪問下標(biāo)的操作:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0]++
testScores["Brian"]?[0] = 72
// 結(jié)果為:
// "Dave" array = [91, 82, 84] 
// "Bev"   array = [80, 94, 81]

// 多套一層可以這樣:
var dict : Dictionary<String, Array<Int>>?
dict = {"Ryan":[1,2,3], "Chris":[4,5,6]}
dict?["Ryan"]?[0] = 0
dict?["Chris"]?[2] = 10
// 結(jié)果為:
// "Ryan"為0,2,3
// "Chris"為4,5,10

多層鏈接

看了上面的例子可能沒有太多的感受, 最后也還是要用if let, 感覺沒有太明顯的優(yōu)勢. 這是因?yàn)槲覀冞€沒有講到鏈, 鏈這個(gè)字就意味著我們可以持續(xù)寫下去, 只要得到的值是可選類型, 我們就能鏈上來.

還是用上面的那些類和實(shí)例, 來感受一下鏈?zhǔn)降拇a:

// 因?yàn)橹百x值的address并沒有成功, 所以此時(shí)還是空的
if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.") // 執(zhí)行這一行
}

// 賦上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).")  // 執(zhí)行這一行
} else {
    print("Unable to retrieve the address.")
}

我們可以看到, 在多層次的可選類型鏈中, 只需要一個(gè)if let就能取出最終的返回值來進(jìn)行判斷, 可以省略很多的中間步驟, 直達(dá)我們的目的地, 最后以官網(wǎng)用鏈?zhǔn)将@得返回值的例子結(jié)尾:

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"."

至此, 可選類型鏈就已經(jīng)結(jié)束, 相信我們都會(huì)在以后的代碼中頻繁使用這一機(jī)制, 畢竟它能夠讓我們的代碼更安全, 更簡潔. 具體細(xì)節(jié)還是慣例查看官方文檔

錯(cuò)誤處理(Error Handling)

不管是什么語言, 只要是認(rèn)真開發(fā)一個(gè)作品都是需要面對錯(cuò)誤處理的. 之前稍稍提過一點(diǎn), Swift和ObjC不一樣, Swift的標(biāo)準(zhǔn)錯(cuò)誤處理靠拋出錯(cuò)誤(嗯, 就是錯(cuò)誤, 畢竟它的protocol叫ErrorType而不是ExceptionType), 而不是靠返回值或者參數(shù). 同時(shí)之前也提過一點(diǎn)Swift是面向協(xié)議編程的, 所以, 如果要拋出錯(cuò)誤的話, 那么就要讓拋出的對象實(shí)現(xiàn)ErrorType協(xié)議.

其實(shí)只要你愿意的話, 繼續(xù)依賴返回值來控制錯(cuò)誤也是可以的, 因?yàn)槲覀冇辛嗽M(tuple)這個(gè)利器. 至于為什么用throws來拋出錯(cuò)誤, 主要還是讓接口的調(diào)用方能夠很明確這個(gè)接口可能出現(xiàn)的錯(cuò)誤是什么.

官方給出一個(gè)自動(dòng)售貨機(jī)買小吃的例子, 可能出現(xiàn)的錯(cuò)誤包括: 不存在該食品, 錢不夠和庫存不足.

錯(cuò)誤的展示和拋出(Representing and Throwing Errors)

如官網(wǎng)所說, ErrorType只是一個(gè)空的協(xié)議, 其主要作用還是一個(gè)指示性的作用, 說明這里可能會(huì)出現(xiàn)錯(cuò)誤. 同時(shí), 官方比較推薦的做法是用枚舉來指示錯(cuò)誤, 例如下面這個(gè)例子:

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

處理錯(cuò)誤

上一節(jié)說了拋出錯(cuò)誤, 那么拋出了就要處理, Swift有四種方法來處理錯(cuò)誤:
1). 傳遞給函數(shù)的調(diào)用方
2). 用do-catch語句來處理
3). 把錯(cuò)誤當(dāng)做optional值來處理
4). 斷言判定錯(cuò)誤不發(fā)生.
下面的小節(jié)會(huì)分別講述這4種處理方法. 特別講一下自己拋出或者處理異常的時(shí)候,
還要用到try(或者try?, try!), 這個(gè)稍后會(huì)細(xì)講.

用可拋出錯(cuò)誤函數(shù)來傳遞錯(cuò)誤(Propagating Errors Using Throwing Functions)

翻譯起來很奇怪, 其實(shí)就是說一個(gè)函數(shù)拋出錯(cuò)誤說明它不處理這個(gè)錯(cuò)誤, 也就要傳遞出去, 讓調(diào)用方來處理. 如果函數(shù)沒有聲明為可拋出, 那么所有內(nèi)部的錯(cuò)誤都要自己處理掉.
聲明的語法如下:

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

先看一個(gè)拋出異常的例子:

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:) 這個(gè)方法會(huì)根據(jù)傳入的參數(shù)拋出3種錯(cuò)誤(這里的guard的優(yōu)勢體現(xiàn)的很明顯, 可以試試用if let來寫會(huì)是什么情況...), 所以我們調(diào)用這個(gè)函數(shù)大概就是這個(gè)德行:

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {  // throws說明繼續(xù)拋出
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)  
}

這里用try而不是throw, 因?yàn)閠hrow后面要接具體的異常, 而后面的方法并不是一定返回異常的, 所以用try.

用do-catch處理異常

如果要自己消化掉錯(cuò)誤, 就要用到do-catch(涉及到具體的語句還是得try). 先看下具體的語法吧:

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

注意, 這里try和別的地方不一樣, 只能try一條語句, 而不可以用try{}來包圍起來. 官方給出下面的例子, 在我的Xcode7.2上會(huì)報(bào)枚舉沒有窮盡的問題, 但是實(shí)際上已經(jīng)窮盡了, 不明白是什么問題, 為了繼續(xù)走下去我加上了一個(gè)分支:

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.") // 觸發(fā)這個(gè)error
} catch { // 兜底的catch
    print("UNKNOW ERROR")
}

錯(cuò)誤轉(zhuǎn)換為可選值(Converting Errors to Optional Values)

之前提過, try還有兩種變種, try?和try!. 如果用try?來處理錯(cuò)誤的話, 會(huì)把錯(cuò)誤轉(zhuǎn)換為一個(gè)可選值. 所以, 如果在執(zhí)行try?語句的時(shí)候拋出了錯(cuò)誤, 那么語句執(zhí)行的結(jié)果就是nil(即使原本沒有返回值也會(huì)返回nil, 這個(gè)之前的章節(jié)講過). try!則是不向調(diào)用方拋出錯(cuò)誤了. 這個(gè)下一小節(jié)講.

先來看官方的例子:

func someThrowingFunction() throws -> Int {
    // ...
}
 
let x = try? someThrowingFunction()  // x的類型為Int?, 而不是Int
 
let y: Int?  // 常量不會(huì)在聲明的時(shí)候自動(dòng)置為nil, 糾正一下在構(gòu)造器里的錯(cuò)誤描述
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
}
去除錯(cuò)誤傳遞(Disabling Error Propagation)

如果你知道一個(gè)聲明為可拋出錯(cuò)誤的函數(shù)或方法, 實(shí)際上不會(huì)拋出錯(cuò)誤, 就會(huì)用到這種處理方法. 所以如果你判斷失誤, 就會(huì)有runtime error了.

官方給出的例子是需要從給定路徑加載圖片, 如果加載失敗就拋出錯(cuò)誤. 有的時(shí)候, 你很確定這張圖片是肯定存在的, 比如隨應(yīng)用一起下載的圖片, 在這種情況就比較適合去除錯(cuò)誤傳遞了.

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

執(zhí)行清理行為(Specifying Cleanup Actions)

在處理錯(cuò)誤的時(shí)候可能要關(guān)閉一些資源, 例如關(guān)閉掉打開的文件(又是這個(gè)例子). 有時(shí)候可能代碼寫多了就會(huì)忘記寫, 導(dǎo)致出現(xiàn)一些問題, 所以Swift里面引入了一個(gè)叫defer(延遲)的關(guān)鍵字, 這個(gè)關(guān)鍵字可以在代碼塊要結(jié)束的時(shí)候執(zhí)行(所謂代碼塊簡單來說就是用大括號(hào)包起來的代碼). 所以, 不管是拋出錯(cuò)誤, 還是return, break, 一旦要離開就開始執(zhí)行defer的代碼.
注意: 如果多個(gè)defer在一起, 我這邊測試情況是, 先執(zhí)行后面的, 再執(zhí)行前面的, 并不是按照編碼的順序. 因此, 可以推斷Swift應(yīng)該是把需要defer的代碼push到棧里面, 之后一個(gè)個(gè)執(zhí)行. 至于為什么要這樣, 我猜測了幾種可能性都不能很好地解釋, 估計(jì)是某些情況打開多個(gè)資源, 子元件又互相有依賴, 那么先釋放掉最后創(chuàng)建的肯定是相對安全的, 因?yàn)閯?chuàng)建資源1的時(shí)候還沒有資源2, 說明資源1,2共存是可以接受的, 但是只有2沒有1則不一定了.

另外需要說明的是, defer里面不能出現(xiàn)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.
    }
}
// 我自己寫了一個(gè)更加簡明的例子:
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
// 結(jié)果輸出:
2+4=6
1+2=3

錯(cuò)誤處理到這邊差不多了, 比較推薦去看下WWDC上的相關(guān)視頻, 細(xì)節(jié)還是參考官方文檔

2016/03/09補(bǔ)充內(nèi)容:
關(guān)于錯(cuò)誤處理有這么個(gè)情況: 假如你寫一個(gè)函數(shù), 函數(shù)接受一個(gè)閉包, 并執(zhí)行它, 如果這個(gè)閉包會(huì)拋出異常, 那么負(fù)責(zé)拋出呢, 如果是閉包拋出, 那么此函數(shù)的調(diào)用方怎么知道這個(gè)函數(shù)是否拋出異常, 拋出什么異常? 甚至再加一個(gè)條件, 如果這個(gè)函數(shù)異步執(zhí)行這個(gè)閉包呢?
第一個(gè)問題的答案是, 誰出錯(cuò)誰拋出, 但是, 函數(shù)要加一個(gè)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, 完全理解有一點(diǎn)難度.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容