可選鏈和錯(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è)對(duì)象的類型是可選的, 按之前提到的我們?cè)谑褂弥岸家酶袊@號(hào)(!)進(jìn)行強(qiáng)制拆包, 而可選類型鏈在處理可能為nil的對(duì)象的時(shí)候, 要優(yōu)雅很多, 它會(huì)在對(duì)象為nil的時(shí)候返回nil(這個(gè)時(shí)候是不是很像ObjC里面的機(jī)制了呢?), 所以, 我們?cè)谑褂玫臅r(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è)例子來對(duì)比強(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è)對(duì)象的下標(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.")
}
再來看看對(duì)可選類型的訪問下標(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è)作品都是需要面對(duì)錯(cuò)誤處理的. 之前稍稍提過一點(diǎn), Swift和ObjC不一樣, Swift的標(biāo)準(zhǔn)錯(cuò)誤處理靠拋出錯(cuò)誤(嗯, 就是錯(cuò)誤, 畢竟它的protocol叫ErrorType而不是ExceptionType), 而不是靠返回值或者參數(shù). 同時(shí)之前也提過一點(diǎn)Swift是面向協(xié)議編程的, 所以, 如果要拋出錯(cuò)誤的話, 那么就要讓拋出的對(duì)象實(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)建的肯定是相對(duì)安全的, 因?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)難度.