寫程式難免有錯,有人說程式設計師的工作,大概只有一半的時間在開發新功能,另一半的時間在喝下午茶。哦,不是啦,是在 debug,也就是所謂的修正錯誤。不過錯誤其實有兩種,剛剛提到 debug 解決的錯誤全是工程師該死,自己製造的 bug。但是這世上,其實還存在另一種無法避免,只能特別處理的錯誤,為此 Swift 特別發明了 Error Handling 的語法來幫助我們。
無法避免,只能特別處理的錯誤
什麼是無法避免,只能特別處理的錯誤呢 ? 比方我們做了一個影響你終生幸福的 App,判斷你能否追到 Angelababy,畫面如下 :
在這個畫面,使用者可輸入自己的條件,包含了星座,年收入,年齡和是否真心,然後再按下按鈕送出,即可知道自己和 Angelababy 是緣定三生,還是有緣無份。
想當然,失敗的機率很高,失敗並不可恥,重點是我們要知道為什麼失敗,如此才能設法補救。比方以下幾點都是失敗的可能原因:
- 年收入太少,養不起 Angelababy。
- 星座不合,不是跟她最合得來的水瓶座。
- 年紀太小,不夠成熟。
這些都是我們要特別處理的錯誤。一般一個完整的 App,都會有許多頁面有類似這樣的錯誤發生,因此我們需要寫程式特別處理。
定義回傳錯誤的 Function
讓我們繼續以剛剛的 Angelababy App 為例,為了判斷是否追得到 Angelababy,我們另外定義 function goAfterAngelababy
做判斷。在 function 裡我們將判斷使用者輸入的內容是否符合 Angelababy 的擇偶條件,將結果以字串回傳,然後再比對字串做不同的處理,比方以下例子:
- 回傳「成功」,顯示 Angelababy 的電話號碼,可以播電話表白了!
-
回傳「失敗:錢太少」,請他趕快來跟彼得潘學 iOS App 開發,做一個像 Angry Bird 那樣既賺大錢又討 Angelababy 歡心的 App。
-
回傳「失敗:星座不合」,請他趕快投胎,祈禱下次出生是水瓶座,還來得及跟 Angelababy 談姐弟戀。
或者,我們也可以另外用 enum
定義代表結果的型別 GoAfterGirlResult
,以 case poorProblem
表示錢太少,tooYoungProblem
表示年紀太小,然後再讓 function goAfterAngelababy
回傳型別 GoAfterGirlResult
的結果。
enum GoAfterGirlResult {
case success
case poorProblem
case tooYoungProblem
case notAquariusProblem
case falseHeartProblem
}
剛剛的做法雖然可行,卻藏著許多令人不安的缺點:
- 為了回傳結果和表達錯誤,我們有許多做法,可以回傳字串,也可以回傳
enum
型別,並沒有一個標準的方法。 - 我們可能忘了處理 function 回傳的錯誤,使得程式變得不安全,使用者體驗不好。
- 呼叫 function 的人,無法一眼看出 function 做的事可能失敗,會回傳錯誤,除非他認真研讀 function 的定義或注解。因此他也就更容易忘了處理錯誤,或是誤以為一定成功,寫出邏輯有問題的程式碼。(就好像你天真地以為一定能追到 Angelababy,但其實是天大的誤會)
這些問題,Swift 都看在眼裡,記在心上,所以它特別發明 Error Handling,幫助我們解決這些問題。
Error Handling 的基本概念
Swift 定義了一個叫 Error
的 protocol,它希望從今以後,我們想在程式裡表達的錯誤都由遵從 Error protocol 的型別來定義。因此,我們可用 class
,struct
或 enum
定義錯誤的型別,只要它遵從 Error protocol。不過大部分的時候,我們想表達的錯誤都是像登入失敗的五種原因,追女生失敗的一百種原因這類可表達成清單的例子,所以我們常用 enum
來定義。
接下來,就讓我們改寫剛剛的例子,我們用 enum
定義了遵從 Error protocol 的型別 GoAfterGirlError
,用它來表達追求女生失敗的各種原因。
enum GoAfterGirlError:Error {
case poorProblem
case tooYoungProblem
case notAquariusProblem
case falseHeartProblem
}
到時候當程式判斷錯誤發生時,必須用關鍵字 throw
丟出錯誤。而且唯有乖乖遵從 Error protocol 的型別,才能被當成錯誤丟出。比方當錢太少時,我們即可用 throw GoAfterGirlError.poorProblem
丟出錯誤。
而當 function 裡的程式碼有可能丟出錯誤時,這個 function 的定義還必須加上 throws
,加在 )
後,警告大家他很危險,有可能丟出錯誤 :
func goAfterAngelababy(money:Int, age:Int) throws {
最後,當我們呼叫的 function 有可能丟出錯誤,也就是它有加上 throws
時,我們還要補上 try
才能呼叫。try
的中文意思就是試一試的意思,雖然有可能失敗,但只要有一絲絲成功的可能,我們都願意一試。
try goAfterAngelababy(money: 1000, age: 30)
如果忘了加上 try
,將顯示紅色錯誤 Call can throw,but it is not marked with ‘try’ 的錯誤訊息提醒我們。
因此,Swift 的 Error Handling 機制將貼心地解決我們剛剛提到的許多問題,丟出錯誤的做法統一用 throw
,呼叫有可能出錯的 function 時,一定要加上 try
,使我們明白它是個危險的 function。除此之外,Swift 還強制要求我們錯誤一定要處理,不處理將產生 compile error 提醒我們。(待會介紹)
定義可能丟出錯誤的 Function
剛剛我們學會了如何定義錯誤,定義了錯誤型別 GoAfterGirlError
。接著就讓我們實際定義一個可以將錯誤丟出的 function goAfterAngelababy 吧。
func goAfterAngelababy(money:Int, age:Int) throws {
guard money > 10000 else {
throw GoAfterGirlError.poorProblem
}
guard age > 18 else {
throw GoAfterGirlError.tooYoungProblem
}
print("我追到 Angelababy 了!")
}
function goAfterAngelababy 可能丟出錯誤,所以我們必須在 ) 後加上 throws
。忘了 throws
,將看到以下可怕的紅色錯誤。
在 function goAfterAngelababy 裡我們利用 guard
判斷是否符合 Angelababy 的擇偶條件。比方當你年收入不到一萬元時,我們將丟出錯誤 poorProblem
,提醒你多賺一點。
值得注意的,一旦在 function 裡丟出錯誤,就會離開 function,不會再執行接下來的程式碼,就好像 return 的效果。(既然都追不到了,就趕快消失在 Angelababy 的眼前,不要再打擾她了。) 所以除非一切順利,完全沒有錯誤,我們才能幸福地印出 「我追到 Angelababy 了!」 的文字訊息。
從這個例子,我們也可發現在開發 App 時,很多 function 都可採用類似的寫法,尤其是表單輸入的頁面。比方新增一個女朋友,我們往往要做很多檢查,確定欄位內容都正確後才新增和交往。此時即可採用剛剛的寫法,利用大量的 guard
做檢查,一發現問題就丟出錯誤。完全沒有錯誤才會執行 function 最後那段建立資料的程式碼。
當丟出錯誤的 function 有回傳資料時,回傳的 ->
要接在 throws
後,然後再接回傳型別,如以下例子所示。而當丟出錯誤時,資料也就不會回傳,因為根本不會執行到 return 的程式碼。
func goAfterAngelababy(money:Int, age:Int) throws -> String {
guard money > 10000 else {
throw GoAfterGirlError.poorProblem
}
guard age > 18 else {
throw GoAfterGirlError.tooYoungProblem
}
return "我追到 Angelababy 了!"
}
有錯一定要處理,知錯能改,善莫大焉
學會了如何丟出錯誤,下一步,就讓我們勇敢地面對錯誤,學習處理它的4種方法。等等,那我們可不可以不處理錯誤呢 ?
當然不可以 ! 請記得,寫程式和做人一樣,有錯一定要處理,不能視若無睹,一錯再錯。比方以下例子,function buttonPressed 將在使用者按下按鈕後觸發,我們在其中利用 try
呼叫有可能出錯的 function goAfterAngelababy。但是我們呼叫後就不管它了,也就是到時候即使錯誤發生,也不做任何處理。這樣是不對的,因此馬上有報應,出現紅色錯誤訊息 Errors thrown from here are not handled。Swift 強制我們處理錯誤,如此我們才能寫出更好更安全的程式。
處理錯誤的第一種方法:將燙手山芋交給下一個人處理(Propagating Errors)
面對問題時,我們總是習慣逃避。其實在寫程式時,這也不失為一個好方法。你可以選擇不自己處理,將錯誤交給下一個人處理。Swift 不在乎誰處理,只要最後有人處理就好。比方以下例子:
func goAfterGirl(money:Int, age:Int) throws {
try goAfterAngelababy(money: money, age: age)
try goAfterVivian(money: money, age: age)
print("兩個女朋友剛剛好")
}
我們寫了一個追女生的 function goAfterGirl,在裡面我們貪心地追求 Angelababy 和 Vivian。(因此我們也寫了另一個跟 goAfterAngelababy 類似的 function,goAfterVivian) 由於可能失敗,所以我們必須使用 try
來呼叫 goAfterAngelababy
和 goAfterVivian
。但是這一次紅色錯誤訊息不見了,因為 function goAfterGirl 也加了 throws
,表示它可以丟出錯誤。當 goAfterAngelababy
或 goAfterVivian
丟出錯誤時,goAfterGirl
會接手錯誤,再把錯誤丟回給當初呼叫它的人,就好像傳球一樣,所以我們變成要在呼叫 goAfterGirl
時處理錯誤。同樣的,一旦出錯就會離開 function,所以如果 goAfterAngelababy
失敗,接下來也不會再執行 goAfterVivian
。
由於我們的程式都是一連串 function 的執行,可能 function A 呼叫 function B,function B 呼叫 function C,所以套用剛剛的技巧,我們可讓 function C 將錯誤丟回給 B,B 再丟回給 A,完美地推卸責任。但是,請記得,錯誤還是存在,只是延後處理罷了。
處理錯誤的第二種方法:勇敢地自己處理錯誤(do catch)
最後還是得有人抱著我不入地獄誰入地獄的勇氣處理錯誤。因此,這就是我們現在要學習的 do catch。
@IBAction func buttonPressed(_ sender: UIButton) {
do {
try goAfterAngelababy(money: 100, age: 25)
try goAfterVivian(money: 100, age: 25)
print("為了給她們幸福,我要白天寫 iOS,晚上寫 Android")
}
catch {
print("我知道她們不愛我,她的眼神,說出她的心。")
}
print("不管有沒有追到,我都要繼續寫 iOS App")
}
透過 do catch,我們可在 do
的 { }
裡執行我們想做的事,包含利用 try
呼叫那些有可能丟出錯誤的 function。然後再經由 catch { }
裡的程式碼處理錯誤。
如果很不幸地,try
呼叫的 function 丟出錯誤,程式將直接跳到 catch
的 { }
處理錯誤。
比方如果 Angelababy 拒絕了我們,她不滿意我們年薪才 100 塊,丟出錯誤 GoAfterAngelababyError.poorProblem
,這時程式將離開 do
的 { }
,跳到 catch
的 { }
執行處理錯誤的程式碼。而當 catch { }
裡的程式執行完後,才會繼續執行第 63 行的程式碼。
如果是順利地沒有任何錯誤發生,也就是 do { }
裡的程式碼都執行了,接著將跳到 catch
的 }
後繼續執行。
剛剛的寫法,catch
將抓取所有的錯誤,所以不管是錢的問題還是年紀問題,都可以一網打盡。但是它有個很大的缺點,我們只知道錯了,卻不知錯在哪,所以很難改進。除非你追不到就換目標,改追下一個。
身為一個有恆心有毅力的真男人,我們應該要再接再厲,對方哪裡不滿意,我們就改進。所以可以改成以下寫法,在 catch
後接某種錯誤,比方 catch GoAfterGirlError.poorProblem 只會補捉錢太少的錯誤。若是錢的問題,那倒還容易解決,只要多工作,同時寫 iOS,Android,Windows 三種 App,即可抱得美人歸。
@IBAction func buttonPressed(_ sender: UIButton) {
do {
try goAfterAngelababy(money: 100, age: 25)
try goAfterVivian(money: 100, age: 25)
print("為了給她們幸福,我要白天寫 iOS,晚上寫 Android")
}
catch GoAfterGirlError.poorProblem {
print("為了多賺一點,我要白天寫 iOS,晚上寫 Android,半夜寫 Windows")
}
catch GoAfterGirlError.tooYoungProblem {
print("我願意等待,等到 80 歲也願意!")
}
catch GoAfterGirlError.notAquariusProblem {
print("不是水瓶座,只好趕快投胎,祈禱來生是水瓶座")
}
catch GoAfterGirlError.falseHeartProblem {
print("不能玩玩而已,我要認真 !")
}
catch {
print("我知道她們不愛我,她的眼神,說出她的心。")
}
print("不管有沒有追到,我都要繼續寫 iOS App")
}
剛剛的這個寫法,我們有 5 個 catch
。它將由上而下依序檢查,只要其中一個 catch
抓到錯誤,即會執行它 { }
裡的程式碼,之後的 catch
則不再執行。比方下圖即為發現 poorProblem
時的程式執行流程。
也許有人會覺得最後一個 catch
可以拿掉,但是對不起,你猜錯了。前面 4 個有名有姓的 catch
都可以拿掉,就是最後一個無名的不行。
錯誤訊息告訴我們,Errors thrown from here are not handled because the enclosing catch is not exhaustive (全面的)。它的意思是我們的 catch
必須檢查到所有的錯誤,不能有漏網之魚,這樣才能保證寫出來的程式不會有問題。也許你會覺得奇怪,明明我們已經把追女生可能失敗的四種錯誤都分別 catch
了,為何還需要最後完全不指定錯誤的 catch
。這是因為 Swift 只知道我們的 function 會丟出錯誤,但它不知道是哪種錯誤,它會覺得可能還有別的錯誤,所以我們要再補上不指定錯誤的 catch
,由它來補捉所有我們沒有指名到的錯誤。
處理錯誤的第三種方法: 回傳 nil 的try?
前面教的 do catch 是我們最常處理錯誤的方法,就好像每次吵架,我們最常使用的招術就是買禮物給對方。不過還是有一些別的方法,比方現在要介紹的 try?
。
@IBAction func buttonPressed(_ sender: UIButton) {
if (try? goAfterAngelababy(money: 100, age: 25)) == nil {
print("追 Angelababy 失敗")
}
if (try? goAfterVivian(money: 100, age: 25)) == nil {
print("追 Vivian 失敗")
}
print("不管有沒有追到,我都要繼續學 Swift。女朋友只是一時,Swift 卻是一輩子。")
}
在 Swift 的世界,看到 ?
,馬上讓人聯想到叫人又愛又恨的 optional 。沒錯,try?
真的跟 optional 有關。當我們用 try?
呼叫 function 時,不再需要用 do catch 補捉錯誤。倘若不幸錯誤真的發生,它將回傳 nil。如果成功,它將回傳東西。因此我們可經由判斷它回傳的結果是不是 nil
得知是否成功,如以上例子所示。(記得要加 ( ) ) 如果成功了話,我們並不在乎它回傳的東西,因為它沒有意義,我們也用不到。
雖然可以判斷是否失敗,不過比起 do catch,它還是略遜一籌。我們只知道失敗,無法知道為什麼失敗。
既然它那麼遜,我們什麼時候會用到 try?
呢 ? 比起 do catch,它的程式碼更為精簡,只有一行,它適合使用在你想呼叫 function,卻不想管為什麼錯,甚至你還可以不處理錯誤,就讓一切隨風而去,如以下例子:
@IBAction func buttonPressed(_ sender: UIButton) {
try? goAfterAngelababy(money: 100, age: 25)
try? goAfterVivian(money: 100, age: 25)
print("不管有沒有追到,我都要繼續學 Swift。女朋友只是一時,Swift 卻是一輩子。")
}
處理錯誤的第四種方法: 務必要百分百成功的 try!
最後,我們還有一招處理錯誤的究極絕招。非到最後關頭,請勿輕易使用。因為,它很危險,它的名字叫做 try!
。在 Swift 看到 !
,就像在 Neverland 看到虎克船長一樣,要馬上想到危險。
@IBAction func buttonPressed(_ sender: UIButton) {
try! goAfterAngelababy(money: 100000000, age: 25)
try! goAfterVivian(money: 100000000, age: 25)
print("我的年收入1後面有很多0,沒有追不到的女生!")
}
唯有當你有百分百把握一定成功時,才能使用 try!
呼叫加了 throws
的 function。比方以上例子,當你的年收入是1後面很多0時,才能這樣做。
如果你不幸失敗了話,那很抱歉,你將面對 Swift 帶給我們的最慘痛教訓,App 閃退 !
總結
現在,我們已經學會了如何定義錯誤,如何定義丟出錯誤的 function,如何呼叫可能出錯的 function,以及最重要的,出錯時如何處理。有了 Swift 的 Error Handling 技術加持,未來我們將能寫出更安全,問題更少的 App 。關於 Error Handling 或 iOS App 開發的相關技術,大家若有任何問題,可在這裡留言。也歡迎隨時聯絡彼得潘。當彼得潘回答大家的問題時,其實也在找答案的過程中精進學習,增長了自己的功力,和大家交了朋友,獲得再多錢也買不到的回報和收獲。