Swift 中的錯(cuò)誤處理機(jī)制

如果大家覺得這篇文章為你提供了一些有用的信息, 請點(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ī)制, 寫出更加健壯的程序_

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芙扎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子湿镀,更是在濱河造成了極大的恐慌赋朦,老刑警劉巖奇徒,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡室琢,警方通過查閱死者的電腦和手機(jī)蹂季,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門冕广,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人偿洁,你說我怎么就攤上這事撒汉。” “怎么了涕滋?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵睬辐,是天一觀的道長。 經(jīng)常有香客問我,道長溯饵,這世上最難降的妖魔是什么侵俗? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮丰刊,結(jié)果婚禮上隘谣,老公的妹妹穿的比我還像新娘。我一直安慰自己啄巧,他們只是感情好寻歧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秩仆,像睡著了一般熄求。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逗概,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天弟晚,我揣著相機(jī)與錄音,去河邊找鬼逾苫。 笑死卿城,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铅搓。 我是一名探鬼主播瑟押,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼星掰!你這毒婦竟也來了多望?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤氢烘,失蹤者是張志新(化名)和其女友劉穎怀偷,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體播玖,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡椎工,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜀踏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片维蒙。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖果覆,靈堂內(nèi)的尸體忽然破棺而出颅痊,到底是詐尸還是另有隱情,我是刑警寧澤局待,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布斑响,位于F島的核電站吗讶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恋捆。R本人自食惡果不足惜照皆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沸停。 院中可真熱鬧膜毁,春花似錦、人聲如沸愤钾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽能颁。三九已至杂瘸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伙菊,已是汗流浹背败玉。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镜硕,地道東北人运翼。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像兴枯,于是被迫代替她去往敵國和親血淌。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容