如果大家覺得這篇文章為你提供了一些有用的信息, 請點(diǎn)個(gè)贊_
希望我們在 iOS 開發(fā)的道路上越走越精彩_
作者微博: weibo.com/dorayakitech
作者GitHub: github.com/magiclee203
對于錯(cuò)誤處理, Objective-C 中已經(jīng)提供了一套機(jī)制, 那便是通過 NSError 來處理錯(cuò)誤相關(guān)的信息. 通常情況下, 如果 OC 中的某個(gè)方法失敗了, 那么一般會返回 nil 來表明該方法失敗, 同時(shí)傳出一個(gè) NSError** 變量, 如果對錯(cuò)誤感興趣, 可以捕捉這個(gè)變量以進(jìn)行后續(xù)的處理.
舉個(gè)例子: NSString 有個(gè)初始化方法
- (instancetype)initWithContentsOfFile: (NSString *)path
encoding: (NSStringEncoding)enc
error: (NSError **)error;
這是通過讀取文件來初始化字符串的方法, 很顯然是可能失敗的. 一旦讀取失敗, 該方法返回 nil, 如果對錯(cuò)誤感興趣, 可以捕獲 error 進(jìn)行后續(xù)的處理, 如果不想處理的話, 傳入 NULL 即可.
在 Swift 2.0 之前, Swift 提供了一個(gè)類似的類, 即 NSErrorPointer 來表示 OC 中的 NSError**(看看人家這名字起的, 太所見即所得了@_@). 上述的初始化方法在 Swift 中的翻譯版本如下:
convenience init?(contentsOfFile path: String,
encoding enc: UInt,
error: NSErrorPointer)
從 Swift 2.0 開始, 這套機(jī)制就改天換地了, error 這個(gè)參數(shù)被移除了, 取而代之的是一套叫做 throw - catch 的錯(cuò)誤處理機(jī)制.
從比較容易入手的角度來考慮的話, 其實(shí)可以把 throw - catch 想成是一種程序跳轉(zhuǎn)語句. 一旦程序在執(zhí)行過程中拋出了異常(即 throw), 那么程序會立即跳轉(zhuǎn)到捕獲異常(catch)的地方繼續(xù)處理, 而不會發(fā)生crash. 這種有效的跳轉(zhuǎn)保證了程序的順利執(zhí)行, 而且可以對異常進(jìn)行后續(xù)處理, 對于我這種 Swift 的腦殘粉來說, 簡直舉雙手給贊啊!
咳咳, 又跑題了, 我們回到正題上來......
既然叫做錯(cuò)誤處理機(jī)制, 那首先得理清到底什么叫做錯(cuò)誤, 又如何將錯(cuò)誤拋出.
關(guān)于 Error
error 這種東西其實(shí)是人為預(yù)先設(shè)定好的一種消息. 這話如何解讀? 且看下面的小例子:
首先定義一個(gè) Person 類
class Person {
var age: Int
init(age: Int) {
self.age = age
}
}
我們創(chuàng)建一個(gè) Person 類的實(shí)例 p
let p = Person(age: -5)
很顯然這是不合理的, 人的年齡怎么可能為負(fù)數(shù)? 這種情況就是異常的, 且看 Swift 如何處理!!!
咦? 神奇的 Swift 并沒有做出任何異常的指示, 也沒有對這異常進(jìn)行任何的處理, 有些小失望誒...
這是當(dāng)然了! 你不會真的天真的以為 Swift 已經(jīng)可以這么神奇了吧! Swift 怎么可能知道 Person 的真實(shí)意義, 它更不可能知道 age 為負(fù)數(shù)到底表示什么. 至此你應(yīng)該理解 error 是人為預(yù)先設(shè)定的含義了吧.
沒錯(cuò), 到底什么是 error, 何種情況是 error, 其實(shí)都是你告訴 Swift 的.
那到底什么東西才能作為 error 呢? 在 Swift 中有兩種東西可以擔(dān)任 error 的角色:
- NSError. 既然 Cocoa 中的類都可以拿到 Swift 中來使用, 當(dāng)然 NSError 也不例外了.
- 遵守 ErrorType 協(xié)議的對象. 這是 Swift 中特有的. struct, enum 等類型的變量只要遵守了 ErrorType 協(xié)議, 就可以當(dāng)做 error 來使用. ErrorType 協(xié)議中有兩個(gè)必須實(shí)現(xiàn)的屬性: 一個(gè)是 String 類型的 _domain, 另一個(gè)是 Int 類型的 _code. 當(dāng)然, 你不需要關(guān)注這兩個(gè)屬性的實(shí)現(xiàn), 因?yàn)?Swift 已經(jīng)偷偷地為你搞定了.
通常情況下, error 都是枚舉類型, 畢竟枚舉可以清楚地指示出到底發(fā)生了何種 error.
接下來通過上面舉的 Person 類的年齡的例子來說明 Swift 中這套 throw - catch 的機(jī)制到底是如何運(yùn)作的.
首先定義一個(gè) error, 用于指示異常.
enum AgeError: ErrorType {
case ThisManIsInverseGrowth // 用于指示年齡為負(fù)的異常
case ThisManIsSuperman // 用于指示年齡過大的異常
}
要注意, AgeError 這個(gè)枚舉要遵守 ErrorType 協(xié)議才能負(fù)起 error 的責(zé)任.
好了, error 已經(jīng)定義好, 那么究竟要如何使用呢? 要注意, error 是被用來 throw 的, 即一旦異常情況發(fā)生, 你要做的就是將這個(gè) error 拋出去(throw)就可以了!
是不是感覺上面的說法很繞, 這都什么跟什么啊... 請繼續(xù)向下看.
這里定義一個(gè)函數(shù), 用來測試一個(gè) Person 實(shí)例的 age 是否合理.(注意! 下面這個(gè)函數(shù)并沒寫完整, 只是先來演示一下思路)
func testPersonAge(p: Person) {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
}
如何? 現(xiàn)在應(yīng)該理清 error 和 throw 何時(shí)使用了吧. 沒錯(cuò), 就在你認(rèn)為出現(xiàn)異常的地方, 將對應(yīng)的 error 拋出去就 OK 了!
未完待續(xù)......
上面的 testPersonAge 函數(shù)并未寫完, 因?yàn)?Swift 對 error 的 throw 是有著一套規(guī)則的. 該規(guī)則如下:
- throw 要出現(xiàn)在 do ... catch 代碼塊中
既然這套錯(cuò)誤處理機(jī)制叫做 throw - catch, 那么有了 throw, 怎么可以沒有 catch 呢? 完整寫法如下:
func testPersonAge(p: Person) {
do {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
print("終于有個(gè)正常的家伙了...")
} catch AgeError.ThisManIsInverseGrowth {
print("這個(gè)家伙居然逆生長了!!!")
} catch AgeError.ThisManIsSuperman {
print("這個(gè)家伙是超人嗎???")
} catch {
print("這個(gè)家伙還有一些其他不正常的地方...")
}
}
這才是一套完整的 throw - catch 寫法. 在 do 代碼塊中 throw 一個(gè) error, 一旦這個(gè) error 確實(shí)發(fā)生了, 那么就會立即跳轉(zhuǎn)至對應(yīng)的 catch 代碼塊來處理相應(yīng)的 error. 是不是覺得 catch 代碼塊的格式很像 switch 里的 case? 沒錯(cuò), 確實(shí)可以把 catch 當(dāng)成 switch 中的不同 case 來處理, 同理, 在一堆 catch 的最后要提供一個(gè)類似 switch 中的 default 的代碼塊, 以此來捕獲沒有被其他 catch 代碼塊捕捉到的 error.
對上面的函數(shù)來說, 假設(shè)對其進(jìn)行如下操作:
let p = Person(age: -5)
testPersonAge(p)
我們會在打印臺看到輸出如下: 這個(gè)家伙居然逆生長了!!!
沒錯(cuò), 你應(yīng)該發(fā)現(xiàn)了, 一旦有 error 出現(xiàn), 那么 throw 之后的任何語句都不會被執(zhí)行! 因此, print("終于有個(gè)正常的家伙了...")
這條語句在存在 error 的情況下永遠(yuǎn)不會被執(zhí)行到! error 被 throw 后, 對應(yīng)的 catch 代碼塊開始運(yùn)行, 使得程序不會 crash.
上面的 testPersonAge 函數(shù)干了一件這樣的事情: 在該函數(shù)內(nèi)部 throw 了一個(gè) error, 同時(shí)在該函數(shù)內(nèi)部 catch 了這個(gè) error. 可實(shí)際開發(fā)中我們會遇到這樣的情況, 我們不希望 throw 了一個(gè) error 的函數(shù)在內(nèi)部對其進(jìn)行處理, 而是希望可以將這個(gè) error 拋到外面, 由其他的函數(shù)對其進(jìn)行處理. 這個(gè)需求再正常不過了, 可是要怎么破? Swift 為上述情況提供了解決方案, 如下:
func testPersonAge(p: Person) throws -> () {
if p.age < 0 {
throw AgeError.ThisManIsInverseGrowth
} else if p.age > 200 {
throw AgeError.ThisManIsSuperman
}
}
如你所見, 如果函數(shù)內(nèi)部進(jìn)行了 throw 的操作, 但沒有 catch, 那么只要在函數(shù)聲明時(shí)加上一個(gè) throws (不是 throw, 是 throws)就搞定了. 這里我特意補(bǔ)全了函數(shù)的格式, 可以看到, 關(guān)鍵字 throws 要放到參數(shù)列表后, -> 之前. 如此一來, 在調(diào)用這個(gè)函數(shù)時(shí), 你就知道這個(gè)函數(shù)可能會拋出異常, 需要特別關(guān)注.
特殊說明!!
千萬注意!! 帶有 throws 關(guān)鍵字的函數(shù), 其類型發(fā)生了改變! 原來的 testPersonAge 函數(shù)的類型是: (Person) -> ()
, 而有了 throws 后, 類型變?yōu)榱?(Person) throws -> ()
為了下文敘述的簡便性, 我把帶有 throws 關(guān)鍵字的函數(shù)稱作 throws 函數(shù).
不知大家有沒有這樣的疑問, throws 函數(shù)內(nèi)部拋出了 error, 但沒有對其進(jìn)行 catch, 那么這個(gè) error 去哪了? 答案就是, 這個(gè) error 向外部傳播出去了! 于是, 就需要外層的函數(shù)對這個(gè) error 進(jìn)行相應(yīng)的處理.
例如: 函數(shù) A 是 throws 函數(shù), 函數(shù) B 調(diào)用了 A, 那么在函數(shù) B 中就需要對 A 做相應(yīng)的處理.
不知道讀到這有沒有繞暈? 下面繼續(xù)使用上面那個(gè) Person 類的例子來說明.
假設(shè)有如下的函數(shù)想調(diào)用 testPersonAge throws 函數(shù):
func caller() {
let p = Person(age: -5)
testPersonAge(p)
}
注意! 上面的寫法是不正確的!
凡是調(diào)用 throws 函數(shù)時(shí), 都需要使用關(guān)鍵字 try! 這提醒了你, 同時(shí)也提醒了編譯器, 告知這個(gè)函數(shù)是可能拋出 error 的. 正確寫法如下:
func caller() {
let p = Person(age: -5)
do {
try testPersonAge(p)
} catch AgeError.ThisManIsInverseGrowth {
print("這個(gè)家伙居然逆生長了!!!")
} catch AgeError.ThisManIsSuperman {
print("這個(gè)家伙是超人嗎???")
} catch {
print("這個(gè)家伙還有一些其他不正常的地方...")
}
}
這個(gè)格式熟悉嗎? 沒錯(cuò), 這與上面直接在函數(shù)內(nèi)部 throw - catch 的格式是完全一致的! 換種更直接的表示方式來說就是, 你如何使用 throw 來拋出 error, 就如何使用 try 關(guān)鍵字!
沒錯(cuò), 你應(yīng)該想到了, func caller()
函數(shù)還可以寫成如下的表達(dá)方式:
func caller() throws {
let p = Person(age: -5)
try testPersonAge(p)
}
這樣, caller()
內(nèi)部不對 error 做任何處理, 而是拋到了外部, 由調(diào)用 caller()
的函數(shù)對 error 進(jìn)行處理.
你應(yīng)該發(fā)現(xiàn)了, try 很麻煩, 因?yàn)槟阕罱K總會在程序中的某個(gè)地方對拋出的 error 進(jìn)行 catch 處理. 以簡潔為特色的 Swift 提供了簡化流程的解決方案, 那就是 try! 和 try?
這兩者的使用思想與 Optional 類型的使用思想相同, 當(dāng)你很確定某個(gè) throws 函數(shù)一定不會拋出異常時(shí), 那么就使用 try!, 這樣就不再需要寫后續(xù)的 catch 部分了. 當(dāng)然, try! 是把雙刃劍, 一旦你確信不會拋出 error 的函數(shù)拋出了 error, 那就只能看著程序 crash 了, 所以使用 try! 時(shí)要慎重!
一個(gè)折中的解決方案就是 try?. 使用 try? 后, 也不需要寫后續(xù)的 catch 部分, 并且其優(yōu)勢在于一旦函數(shù)拋出了一個(gè) error, 那么程序不會 crash. **需要注意的是, 如果 throws 函數(shù)有返回值, 那么經(jīng)過 try? 后, 這個(gè)返回值會變?yōu)橄鄳?yīng)的 Optional 類型. ** 例如 testPersonAge 函數(shù)的聲明如下: func testPersonAge(p: Person) throws -> Person
.
那么: let person = try? testPersonAge(p)
得到的 person 變量類型是 Person?, 不再是 Person.
同 Swift 中很多數(shù)據(jù)類型相似, 遵守了 ErrorType 協(xié)議的類型與 NSError 是可以橋接的, 這意味著在需要 NSError 的地方, 你可以使用遵守 ErrorType 協(xié)議的 Swift 類型, 反之同理.
好了, Swift 中的錯(cuò)誤處理機(jī)制大體上就是這些, 至于不太常用的 rethrows 這里沒有做介紹, 如有興趣, 歡迎一起探討. 希望大家能夠利用這套機(jī)制, 寫出更加健壯的程序_