「錯誤處理」是程式設計必備的技能之一,我們最早曾經在第2單元第7課 傑森解碼器(JSON) 中用過,就是 do-catch, try 等指令。由於非同步程式常用到外部資源,難免遇到意外狀況,必然需要錯誤處理,所以若想要進一步善用 async/await,就必須先熟悉 Swift 錯誤處理的語法。

錯誤處理和除錯(debug)意義不同,除錯(debug)是要排除程式的語法錯誤或邏輯錯誤,例如標點符號用錯、條件句的true/false子句用顛倒、邊界條件沒有檢查...等等,修正程式無法執行或執行結果與預期不符的問題。

錯誤處理則是對所有已知的「例外狀況」進行處置,以本單元網路程式為例,必須連到外部網站,才能抓取圖片或JSON資料,「正常狀況」下都能順利執行,但如果是網路斷線,或甚至過若干時日,外部網站已關閉,程式要如何正常執行?

舉例而言,如果將本機的WiFi關閉,再執行範例程式3-6a非同步抓圖,會顯示 ProgressView()不停轉圈,而沒有任何提示;範例程式3-2a更慘,會直接被作業系統打斷(閃退)。通常我們稱這樣的程式很脆弱(fragile),一有意外就死掉,或是不夠強健(robust),經不起風吹雨打。

好的App一定是強健的程式,能應付各種狀況,要寫出強健的程式,就必須做好錯誤處理。

我們用上一課的「質因數分解」來寫個範例。根據數學上的定義,「質數」必然是大於1的正整數,「因數」則正負整數均可,但不可為零(任何數都不可除以零),零也不能因數分解。

為了簡單起見,以下範例程式只做「正整數」的因數分解,所以如果遇到零或負整數,則當作已知的錯誤來處理。

// 3-8a 錯誤處理(Error Handling) -- 正整數因數分解
// Revised (based on 3-7b) by Heman, 2022/01/09
import Foundation

enum 因數分解錯誤: Error {
    case 負整數
    case 等於零
}

func 因數分解(_ n: Int) throws -> [Int] {
    if n < 0 {              // 排除零與負數
        throw 因數分解錯誤.負整數
    } else if n == 0 {
        throw 因數分解錯誤.等於零
    } else if n == 1 {
        return [1]
    }
    var 因數: [Int] = [1]    // 1與n是基本的因數
    for i in 2...(n-1) {
        if n % i == 0 {
            因數 = 因數 + [i]
        }
    }
    因數 = 因數 + [n]
    return 因數
}

let 某數 = [-20220109, 0, 20220109, 20220911]
var 時間差: Double = 0.0

let 計時開始 = Date()
print("因數分解 -- 開始時間:\\(計時開始)")
for i in 某數 {
    do {
        let 因數 = try 因數分解(i)
        時間差 = Date().timeIntervalSince(計時開始)
        print("\\(i): \\(因數) 時間差 \\(時間差)")
    } catch 因數分解錯誤.負整數 {
        print("錯誤:暫不支援負整數的因數分解(\\(i))")
    } catch 因數分解錯誤.等於零 {
        print("錯誤:零無法因數分解(\\(i))")
    } catch {
        print("錯誤:其他原因(\\(i))")
    }
}
時間差 = Date().timeIntervalSince(計時開始)
print("[因數分解]共花費時間(秒):\\(時間差)")
print("程式結束:\\(Date())")

從以上實際的程式碼可以看出來,錯誤處理就是對可能發生的例外狀況,加以偵測判斷並撰寫應對的程式碼。

在語法上,「錯誤處理」分為「偵測」與「處理」兩個部分,「偵測」通常以函式為主體,由函式偵測並回報已知的錯誤或例外狀況,「處理」則是根據回報的狀況加以應對。

負責偵測錯誤的函式,在宣告時要在參數後面加上 throws 指令(注意加上 's',是第三人稱單數用的動詞),然後在偵測到的錯誤狀況下,用 throw (祈使句,不加 's')丟出錯誤狀況的代碼: