Swift 程式語言

Swift 入門教學:知錯能改善莫大焉的 Error Handling

Swift 入門教學:知錯能改善莫大焉的 Error Handling
Swift 入門教學:知錯能改善莫大焉的 Error Handling
In: Swift 程式語言

寫程式難免有錯,有人說程式設計師的工作,大概只有一半的時間在開發新功能,另一半的時間在喝下午茶。哦,不是啦,是在 debug,也就是所謂的修正錯誤。不過錯誤其實有兩種,剛剛提到 debug 解決的錯誤全是工程師該死,自己製造的 bug。但是這世上,其實還存在另一種無法避免,只能特別處理的錯誤,為此 Swift 特別發明了 Error Handling 的語法來幫助我們。

無法避免,只能特別處理的錯誤

什麼是無法避免,只能特別處理的錯誤呢 ? 比方我們做了一個影響你終生幸福的 App,判斷你能否追到 Angelababy,畫面如下 :

error-handling-1

在這個畫面,使用者可輸入自己的條件,包含了星座,年收入,年齡和是否真心,然後再按下按鈕送出,即可知道自己和 Angelababy 是緣定三生,還是有緣無份。

想當然,失敗的機率很高,失敗並不可恥,重點是我們要知道為什麼失敗,如此才能設法補救。比方以下幾點都是失敗的可能原因:

  1. 年收入太少,養不起 Angelababy。
  2. 星座不合,不是跟她最合得來的水瓶座。
  3. 年紀太小,不夠成熟。

這些都是我們要特別處理的錯誤。一般一個完整的 App,都會有許多頁面有類似這樣的錯誤發生,因此我們需要寫程式特別處理。

定義回傳錯誤的 Function

讓我們繼續以剛剛的 Angelababy App 為例,為了判斷是否追得到 Angelababy,我們另外定義 function goAfterAngelababy 做判斷。在 function 裡我們將判斷使用者輸入的內容是否符合 Angelababy 的擇偶條件,將結果以字串回傳,然後再比對字串做不同的處理,比方以下例子:

  1. 回傳「成功」,顯示 Angelababy 的電話號碼,可以播電話表白了!

  2. 回傳「失敗:錢太少」,請他趕快來跟彼得潘學 iOS App 開發,做一個像 Angry Bird 那樣既賺大錢又討 Angelababy 歡心的 App。

  3. 回傳「失敗:星座不合」,請他趕快投胎,祈禱下次出生是水瓶座,還來得及跟 Angelababy 談姐弟戀。

或者,我們也可以另外用 enum 定義代表結果的型別 GoAfterGirlResult,以 case poorProblem 表示錢太少,tooYoungProblem 表示年紀太小,然後再讓 function goAfterAngelababy 回傳型別 GoAfterGirlResult 的結果。

enum GoAfterGirlResult {
    case success
    case poorProblem
    case tooYoungProblem
    case notAquariusProblem
    case falseHeartProblem
}

剛剛的做法雖然可行,卻藏著許多令人不安的缺點:

  1. 為了回傳結果和表達錯誤,我們有許多做法,可以回傳字串,也可以回傳 enum 型別,並沒有一個標準的方法。
  2. 我們可能忘了處理 function 回傳的錯誤,使得程式變得不安全,使用者體驗不好。
  3. 呼叫 function 的人,無法一眼看出 function 做的事可能失敗,會回傳錯誤,除非他認真研讀 function 的定義或注解。因此他也就更容易忘了處理錯誤,或是誤以為一定成功,寫出邏輯有問題的程式碼。(就好像你天真地以為一定能追到 Angelababy,但其實是天大的誤會)

這些問題,Swift 都看在眼裡,記在心上,所以它特別發明 Error Handling,幫助我們解決這些問題。

Error Handling 的基本概念

Swift 定義了一個叫 Error 的 protocol,它希望從今以後,我們想在程式裡表達的錯誤都由遵從 Error protocol 的型別來定義。因此,我們可用 classstructenum 定義錯誤的型別,只要它遵從 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’ 的錯誤訊息提醒我們。

error-handling-2

因此,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,將看到以下可怕的紅色錯誤。

error-handling-3

在 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 強制我們處理錯誤,如此我們才能寫出更好更安全的程式。

error-handling-4

處理錯誤的第一種方法:將燙手山芋交給下一個人處理(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 來呼叫 goAfterAngelababygoAfterVivian。但是這一次紅色錯誤訊息不見了,因為 function goAfterGirl 也加了 throws,表示它可以丟出錯誤。當 goAfterAngelababygoAfterVivian 丟出錯誤時,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 行的程式碼。

error-handling-5

如果是順利地沒有任何錯誤發生,也就是 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 時的程式執行流程。

swift error-handling-6

也許有人會覺得最後一個 catch 可以拿掉,但是對不起,你猜錯了。前面 4 個有名有姓的 catch 都可以拿掉,就是最後一個無名的不行。

error-handling-7

錯誤訊息告訴我們,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 閃退 !

error-handling-8

總結

現在,我們已經學會了如何定義錯誤,如何定義丟出錯誤的 function,如何呼叫可能出錯的 function,以及最重要的,出錯時如何處理。有了 Swift 的 Error Handling 技術加持,未來我們將能寫出更安全,問題更少的 App 。關於 Error Handling 或 iOS App 開發的相關技術,大家若有任何問題,可在這裡留言。也歡迎隨時聯絡彼得潘。當彼得潘回答大家的問題時,其實也在找答案的過程中精進學習,增長了自己的功力,和大家交了朋友,獲得再多錢也買不到的回報和收獲。

作者
彼得潘
彼得潘,正職作家,副業講師,深愛 Apple 相關的所有人事物。精通 Swift iOS 程式設計,平日的興趣為桌球,情歌和寫作。除了一天一顆蘋果強身,也努力保持一天研究一項 iOS SDK 技術的習慣。著作: Swift程式設計入門,App 程式設計入門-iPhone,iPad 課程。Line ID: deeplovepeterpan
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。