這節課我們一起來認識一下 Swift 中的錯誤處理
在調用方法和寫一個輪子的時候,總會有各種各樣奇奇怪怪的錯誤,就是已經正常編譯的軟件,也會出現一些不可預期的錯誤。不過,這些錯誤當中,有一些是可以被識別和捕捉的——它們可預期。
可預期的錯誤
為什麼我們說有一些錯誤是可以預料得到的呢?比如說讀取一個文件的時候文件不存在、保存一個文檔的時候目錄不可寫、下載文件的時候網絡無連接、傳送一個參數的時候範圍超出控制……這些錯誤都是可以預料得到的!我們完全沒有必要讓程序在這些錯誤裡崩潰!
那麼,在這些情況下我們就需要一個完善的異常處理機制,把這些可以預料到的異常都給捕捉起來,用友好的方式處理掉,避免我們的程序栽在這些已知的錯誤上。
拋出錯誤和抓住錯誤
我們調用了一個方法,如果必要,那你必須要明白這個方法是有風險的,它有可能會執行失敗——這個時候它就會將失敗的原因拋出來——拋出錯誤。
你必須將它可能拋出錯誤的情況都考慮好,如果這樣報錯,怎麼辦,如果那樣報錯,又怎麼辦!
所以,如果我們創建的方法可能有風險,就一定要用 throws 標記出來,這樣調用的時候我們就可以用 catch 來接住這個炸彈。
創建一個包含錯誤狀態的枚舉
我們用枚舉類型來創建可能的錯誤狀況,只需要讓枚舉遵循 ErrorType 協議即可,其他的都交給編譯器完成。
1 2 3 4 5 |
enum FireError:ErrorType { case overHeat(hot:Double) case outOfAmmo } |
比如說我們這裡寫兩個錯誤,一個是槍管過熱,另一個是沒有子彈了!
有風險的方法
然後我們再來改造 Gun 的 fire() 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { guard ammo > 0 else { throw FireError.outOfAmmo } guard heat < 100 else { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
一個簡單的模型,每次發射之前檢查彈藥數量,檢查槍管溫度,如果都 OK,就射擊,同時槍管溫度增加,彈藥量減少。(這裡溫度設置不一定合常理哈)
處理錯誤
然後我們來調用這個方法11次看看會有什麼樣的情況發生:
1 2 3 4 5 6 7 8 9 10 11 |
var a = Gun() for _ in 0...10 { do { try a.fire() } catch FireError.overHeat(let heat) { print("枪管过热 \(heat) 度!") } catch FireError.outOfAmmo { print("Need reload!") } } |
最終,我們的到的結果是:
1 2 3 4 5 6 7 8 9 10 11 |
bang! bang! bang! bang! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! |
Guard 是 if 嗎?
答:不是。
如你所見,Guard 語法用起來好像很像是 if,但它不是,你可以理解為它是個反的 if , 如果我們反過來這樣寫也是完全可以的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if ammo > 0 { if heat < 100 { print("bang!") ammo-- heat += 20.5 } else { throw FireError.overHeat(hot: heat) } } else { throw FireError.outOfAmmo } } |
看起來是不是很繞?一堆一堆的大括號看著就眼暈!
使用 Guard 則就好像是個守門員——我們叫他“守門模式”,其實看起來就好像是反過來用 if:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if ammo < 0 { throw FireError.outOfAmmo } if heat > 100 { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
這樣雖然一樣了,你注意到沒有?我們把大於小於號給翻轉了!因為這樣才能讓 if 起到我們想要的作用,或者,我們可以這樣用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if !(ammo > 0) { throw FireError.outOfAmmo } if !(heat < 100) { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
這樣大小的邏輯是擺正了,可是多出來的嘆號更讓人頭大了,一不小心還容易擾不過這個彎兒……
所以,我們用 Guard,它也判斷是非,如果“是”則不作用,如果“非”,就執行 else 裡的內容,這樣行內的邏輯對人來說就很直觀了——不再需要曲線救國。
有風險的方法要放在 do – catch 塊裡
就像我前面的例子,我們把 fire 方法改造成能夠拋出異常的方法,那麼調用它的時候就必須把它放進特製的容器當中,這樣編譯器才能搞定和理解那些錯誤。
1 2 3 4 5 |
do { try someFunction that throws } catch error { } catch anotherError { } |
如上,我們在 do 的代碼塊當中來嘗試方法(try),如果正常那皆大歡喜。如果方法拋出了異常,那我們就會執行到 catch 上——這裡其實看起來好像 if else 的嵌套,也好像 switch 的選擇——不過,catch 確實要求把可能會拋出的錯誤都寫出來如果你不這麼做,那麼這個炸彈落地你的程序也就會被炸掉啦!
不過,就像 switch 一樣,你也可以只寫 catch 而不跟錯誤類型,這樣就可以默認匹配所有錯誤了——缺點是你也不能知道發生了什麼錯誤。
與調用者溝通
那我們做這個複雜的動作究竟有什麼意義呢?直接把這些寫成一個狀態不更好?
還真不會更好。因為我們的方法並不一定就是給自己用,一個程序也不會只有一個人完成,當別人調用你的方法的時候,難道非得讓人家先讀一下800字的文檔?在需要的位置拋出一個錯誤,寫清楚錯誤的類型——調用者就可以方便地處理掉這個問題。
傳球!
我們說遇到風險就要 try,而 try 出來的錯誤要 catch!可是沒有 catch 住的炸彈怎麼辦?如果你不想拆彈呢?其實也可以,如果你在你的方法裡調用了一個有風險的方法,而你又不想在這個方法里拆彈,那就不要管它就好了——當然,要記得把你的方法也標記為 throws ,這樣這個炸彈會繼續往下傳!
呃,但你得記住,最終得有個方法來 catch 和拆彈,不然,你的程序還是會被炸掉。
如果一個問題得不到解決,它不會自行消失。
對了,如果你不想讓錯誤拋出,那可以強制 try,使用關鍵字 try! ,這樣就不需要拆彈了……因為遇到炸彈不用扔,直接炸。
本文由 落格博客 原創撰寫:落格博客 » 總會報錯:異常處理
轉載請保留出處和原文鏈接:https://www.logcg.com/archives/1137.html