#31 錯誤傳遞(Error Propagation) -- 非同步質因數分解
關於Swift程式的錯誤處理,還有一個地方要學,就是在throw函式裡面可以呼叫其他throw函式嗎?
前面提過,Swift「錯誤處理」工作分為兩部分,負責「偵測」的函式在宣告時要加 throws 指令,故可稱為 throw函式(原文則稱 "throwing functions"),呼叫函式者(caller)則用do-try-catch來負責「應對」,基本句型如下:
錯誤偵測者(throw):
func 函式(參數) throws -> 回傳類型 {
...
if ... { throw 錯誤代碼 }
...
}
錯誤應對者(do-try-catch):
do {
let x = try 函式(參數)
...
} catch { ... }
錯誤偵測者與錯誤應對者有點像棒球的投手與捕手之間的關係,一個拋(throw),一個接(catch)。
假若有個函式b呼叫錯誤偵測的throw函式a,一般情況下,函式b是呼叫者(caller),應該用 do-try-catch 來應對函式a丟出來的錯誤情況,如下圖。
不過,函式b仍可以宣告為 throw 函式,這時在函式b中呼叫函式a時所抓到的錯誤代碼,就可選擇自己處理,或丟給上一層呼叫者處理 -- 也就是函式b可以只傳遞錯誤代碼,完全不處理,如下圖所示。
這樣就有點像棒球的外野手要傳球回本壘時,可以透過二壘手傳遞一樣,二壘手可以選擇刺殺一壘過來的跑者,或是趕緊傳回本壘避免失分。
我們仍然用「質因數分解」來當作範例說明,在此範例中,「因數分解」宣告為 async throws,相當於上圖的函式a,「非同步質因數分解」也宣告為async throws,相當於上圖函式b,會往上傳遞錯誤代碼,在最後的程式本體中,再來處理錯誤狀況。
// 3-8d 錯誤傳遞:非同步質因數分解
// Revised by Heman, 2022/01/17
import Foundation
func 是質數嗎(_ n: Int) async -> Bool {
if n < 2 { // 只判別大於1的正整數
return false
} else if n == 2 || n == 3 {
return true
}
let 近平方根 = Int(sqrt(Double(n))) //n必須大於等於4
for i in 2...近平方根 {
if (n % i) == 0 {
return false
}
}
return true
}
enum 因數分解錯誤: Error {
case 負整數
case 等於零
}
func 因數分解(_ n: Int) async throws -> [Int] {
if n < 0 { // 排除零與負數
throw 因數分解錯誤.負整數
} else if n == 0 {
throw 因數分解錯誤.等於零
} else if n == 1 {
return [1]
} else if n == 2 || n == 3 {
return [1, n]
}
var 因數: [Int] = [1] // 1與n是基本的因數
let 近平方根 = Int(sqrt(Double(n))) //n必須大於等於4
for i in 2...近平方根 {
if n % i == 0 {
因數 = 因數 + [i]
let 商數 = Int(n / i)
if 商數 != 近平方根 {
因數 = 因數 + [商數]
}
}
}
因數 = 因數 + [n]
return 因數.sorted()
}
func 非同步質因數分解(_ n: Int) async throws -> [Int] {
let 因數 = try await 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if await 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}
let 某數 = [-20220109, 0, 20220109, 20220911, 20223009, 123456789012345, 135791357913579]
let 數量 = 某數.count
var 已完成 = 0
var 時間差: Double = 0.0
var 本地格式 = DateFormatter()
本地格式.dateStyle = .full
本地格式.timeStyle = .long
let 計時開始 = Date()
print("非同步質因數分解 -- 開始時間:\(本地格式.string(from: 計時開始))")
for i in 某數 {
Task {
do {
let 質因數 = try await 非同步質因數分解(i)
時間差 = Date().timeIntervalSince(計時開始)
let 秒數 = String(format: "%.5f秒", 時間差)
print("\(i): \(質因數) 時間差", 秒數)
} catch 因數分解錯誤.負整數, 因數分解錯誤.等於零 {
print("錯誤:僅限正整數的質因數分解(\(i))")
} catch {
print("錯誤:其他原因(\(i))")
}
已完成 = 已完成 + 1
if 已完成 == 數量 {
時間差 = Date().timeIntervalSince(計時開始)
print("[非同步質因數分解]共花費時間(秒):\(時間差)")
print("程式結束:\(本地格式.string(from: Date()))")
}
}
}
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
這個範例程式,除了示範在 throw函式中呼叫另一個 throw函式之外,也示範在 async 函式中呼叫另一個 async 函式,所以包括判斷質數的「是質數嗎()」也宣告為async,想想看,這樣有什麼不同?
這是本單元最後一個文字模式(主控台)的範例程式,特別將時間格式調整為本地模式,並且將秒數改為小數點5位 -- 美化的工作留到最後再做即可。DateFormatter() 的用法可參考第一單元第10課,String(format:)的用法,可以參考第一單元第9課。
輸出結果如下,要注意觀察的地方,就是兩個錯誤代碼其實是從「因數分解()」偵測並拋給「非同步質因數分解()」,再傳遞給主程式處理 -- 顯示錯誤訊息。中間的「非同步質因數分解()」既不偵測,也不處理,只單純傳遞錯誤,所以程式碼很簡潔,不需要 if ... throw,也不需要 do-try-catch。
此外,「非同步質因數分解()」在呼叫非同步的「是質數嗎()」,只需要 await,不需要在外面加一層 Task,這是因為函式本身已經宣告為 async 的關係 -- 在 async 函式中呼叫另一個 async 函式,就是這麼簡單!
非同步質因數分解 -- 開始時間:2022年1月17日 星期一 GMT+8 下午2:02:51
錯誤:僅限正整數的質因數分解(-20220109)
錯誤:僅限正整數的質因數分解(0)
20220911: [97, 208463] 時間差 0.02320秒
20223009: [3, 1499] 時間差 0.02657秒
20220109: [7, 13, 222199] 時間差 0.02858秒
123456789012345: [3, 5, 283, 3851, 7552031] 時間差 9.10102秒
135791357913579: [3, 31, 37, 367, 2906161] 時間差 9.49314秒
[非同步質因數分解]共花費時間(秒):9.493432998657227
程式結束:2022年1月17日 星期一 GMT+8 下午2:03:01
註解
- 在本(第8)課一開始提過,「當throw函式偵測到錯誤(例外狀況),執行到 throw 時,函式會直接返回,不會再繼續往下執行」,例如「因數分解(0)」的情況,函式執行到 if n == 0 { throw ... } 就會返回。
- 那麼,模擬一下「非同步質因數分解(0)」又會是如何呢?
func 非同步質因數分解(_ n: Int) async throws -> [Int] {
let 因數 = try await 因數分解(n)
var 質因數: [Int] = []
for i in 因數 {
if await 是質數嗎(i) {
質因數 = 質因數 + [i]
}
}
return 質因數
}
答案是,執行到第一行 let 因數 = try await 因數分解(n) 就會返回,雖然沒有 throw 指令,但同樣會拋出(其實是傳遞)錯誤代碼,而沒有正常的回傳值。
- 所以簡單地說,Swift程式所謂的「錯誤處理(Error Handing)」,其中的「錯誤」是指函式無法正常回傳值的例外情況,若這些例外情況沒有應對處理好,就可能造成無法預期的結果。