Swift 中的錯誤處理
將可能遇到的異常盡可能扼殺在編譯器是 Swift 在安全性上至始至終貫徹的理念,例如之前提到的可選型以及本文即將討論的錯誤處理 (Error Handling)。
錯誤(Error)
可以簡單的將錯誤劃分為編譯錯誤、邏輯錯誤以及運行時錯誤
-
編譯錯誤
let a = 10 a = 20 // 編譯器報錯: Cannot assign to value: 'a' is a 'let' constantfunc name(v: Int) -> Int { return "hello" }
// 編譯器報錯:Cannot convert return expression of type 'String' to return type 'Int'
* 邏輯錯誤
```swift
let username = "Jay" // 用戶名
let password = "123456" // 密碼
// 定義登錄方法,傳入用戶名和密碼作為參數(shù)
func login(userName: String, password: String, completion: (Bool, Error) -> Void) {
// .....
}
// 調(diào)用 login 方法時,錯誤的將 username 和 password 參數(shù)傳反
login(userName: password, password: userName) { (success, error) in
// ..
}
- 運行時錯誤
/// 除法運算 /// /// - Parameters: /// - dividend: 被除數(shù) /// - divisor: 除數(shù) /// - Returns: 除法計算結(jié)果 func division(_ dividend: Int, _ divisor: Int) -> Int { return dividend / divisor } let result = division(10, 0) print(result) // 運行時報錯: Fatal error: Division by zero
編譯錯誤很容易發(fā)現(xiàn),因為壓根無法編譯通過;邏輯錯誤稍微隱蔽一些,尤其程序員一旦陷入思維定勢即使是很簡單的錯誤都很難被發(fā)現(xiàn),通常需要反復排查以及 Code Review。
運行時錯誤是最為棘手的,往往這類錯誤產(chǎn)生的原因種類繁多,比如 iOS 開發(fā)中常見的:野指針訪問、數(shù)組越界、給不存在的方法發(fā)送消息...
Swift 推出一套錯誤處理機制來嘗試解決運行時錯誤,如果使用得當,可以在一定程度上達成將錯誤扼殺在編譯階段的目的。
錯誤協(xié)議(Error protocol)
在 Swift 標準庫中定義了一個名為 Error 的協(xié)議:
public protocol Error {
var _domain: String { get }
var _code: Int { get }
// Note: _userInfo is always an NSDictionary, but we cannot use that type here
// because the standard library cannot depend on Foundation. However, the
// underscore implies that we control all implementations of this requirement.
var _userInfo: AnyObject? { get }
#if _runtime(_ObjC)
func _getEmbeddedNSError() -> AnyObject?
#endif
}
原則上 Swift 中的任何類型都可以遵循這個 Error 協(xié)議來表示錯誤類型,但出于性能和規(guī)范性考慮我們通常只使用遵循 Error 協(xié)議的枚舉(Enumerations)和結(jié)構(gòu)體(structure)來表示錯誤:
- 使用枚舉定義錯誤類型
enum DivisionError: Error {
case invalidInput(String)
case overflow(Int, Int)
}
- 使用結(jié)構(gòu)體定義錯誤類型
struct XMLParsingError: Error {
enum ErrorKind {
case invalidCharacter
case mismatchedTag
case internalError
}
let line: Int
let column: Int
let kind: ErrorKind
}
拋出錯誤(Throwing errors)
現(xiàn)在已經(jīng)定義好錯誤類型,如何才能在我們的代碼中使用這些錯誤呢?
throw、throws、try 關(guān)鍵字
函數(shù)內(nèi)部通過 throw 拋出自定義 Error,可能拋出 Error 的函數(shù)必須加上 throws 聲明:
// 在參數(shù)和返回值之間加上 throws 關(guān)鍵字
func division(_ dividend: Int, _ divisor: Int) throws -> Int {
if divisor == 0 {
// 使用 throw 關(guān)鍵字拋出自定義的 DivisionError 錯誤
throw DivisionError.invalidInput("0 不能作為除數(shù)!")
}
return dividend / divisor
}
在調(diào)用可能拋出 Error 的函數(shù)時,需要在函數(shù)名之前加上 try 關(guān)鍵字:
let result = try division(10, 0)
print(result)
這次報出的錯誤不再是Fatal error: Division by zero,而是我們自定義的Fatal error: DivisionError.invalidInput("0 不能作為除數(shù)!")
再看蘋果官方文檔的一個例子:
func parse(_ source: String) throws -> XMLDoc {
// ...
throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
// ...
}
let xmlDoc = try parse(myXMLData)
從上面兩個例子不難看出 throws 和 try 總是一起出現(xiàn)的:一旦函數(shù)簽名中出現(xiàn) throws 即表示這個函數(shù)可能會拋出錯誤,此時調(diào)用這個函數(shù)時編譯器將強制添加 try 關(guān)鍵詞,否則無法編譯通過。
但需要注意:使用 try 只是保證編譯通過,編譯器并沒有幫我們自動處理異常,異常的捕獲和處理都需要程序員自己進行。
注:使用結(jié)構(gòu)體定義的 XMLParsingError 可以處理的精細程度要比枚舉定義的 DivisionError 更高,通常來說枚舉類型已經(jīng)足夠用來表達錯誤類型,因此本文接下來都將使用 DivisionError 來演示錯誤類型。
錯誤處理(Handling errors)
在 Swift 中有兩種處理錯誤的方式:
- 使用 do-catch 捕捉 Error
- 不捕捉 Error,在當前函數(shù)增加 throws 聲明,Error 將自動拋給上層函數(shù)
do-catch 直接捕獲 Error
下面是 do-catch 的語法:
do {
let result = try division(10, 0)
print(result)
} catch let DivisionError.invalidInput(msg) {
print(msg)
} catch DivisionError.overflow {
print("越界了")
} catch {
print("其他錯誤")
}
語法很簡單,但有幾點需要注意:
將之前
try division的語句放到do {}內(nèi),一旦try division(10, 0)拋出錯誤,其作用域內(nèi)之后的代碼將不再執(zhí)行,即print(result)不會執(zhí)行如果需要獲取某個 catch 到 Error枚舉的關(guān)聯(lián)值,可以參考
let DivisionError.invalidInput(msg)
將 Error 上拋
如果不想立即處理錯誤,還可以將錯誤上拋,交給上層的函數(shù)處理:
func calculate() throws {
let result = try division(10, 0)
print(result)
}
在調(diào)用 try division(10, 0) 的時候不立即處理錯誤,可以在當前函數(shù) calculate() 加上 throws 關(guān)鍵字,表示將錯誤拋給 calculate()函數(shù)。
此時調(diào)用try calculate(),編譯器依然會提示: Errors thrown from here are not handled。
將錯誤上拋只是將錯誤處理的交給其他函數(shù)來處理,最終依然需要使用 do-catch 進行處理:
do {
try calculate()
} catch DivisionError.invalidInput {
print("非法參數(shù)")
} catch DivisionError.overflow {
print("越界")
} catch {
print("其他錯誤")
}
需要注意:如果一直拋到最頂層的 main 函數(shù)都不進行處理,編譯器不會再進行提醒,而是在運行時直接報錯:
Fatal error: Error raised at top level: 錯誤處理.DivisionError.invalidInput("0 不能作為除數(shù)!")
try?、try!
如果你根本不在乎拋出錯誤的細節(jié),有時我們只是想快速的獲取 division(10, 0) 的值,do-catch 的語法就顯得有些冗長。幸好 Swift 還提供了一種語法糖來簡化整個過程:
var result = try? division(10, 0)
使用 try? 會忽略掉可能拋出異常的細節(jié),并將函數(shù)的返回值包裝為可選型。因此完全等價于下面的代碼:
var result: Int?
do {
result = try division(10, 0)
} catch {
result = nil // 這一句可以省略,因為可選型默認值為 nil
}
如果你依舊嫌 try? 引出的可選型太麻煩,Swift 甚至還提供 try! 語法糖:
var result = try! division(10, 0)
try! 其實就是 try? 的強制解包,如果你能夠確保 division() 的結(jié)果不會為 nil,try! 的確是個漂亮的語法糖。但往往這樣的『確保』是靠不住的,因此要謹慎使用 try!
rethrows
我們知道在 Swift 中,函數(shù)是一等公民,可以作為參數(shù)或返回值參與到另一個函數(shù)中,那如果將一個可能拋出錯誤的閉包作為參數(shù)會怎樣呢?
func calculate(_ number1: Int, _ number2: Int, _ equation: (Int, Int) throws -> Int) -> Int {
return try equation(number1, number2)
}
calculate 函數(shù)的第三個參數(shù) equation 為一個可能拋出錯誤的閉包,在函數(shù)體內(nèi)調(diào)用 equation 時需要加上 try 關(guān)鍵字,并且處理可能出現(xiàn)的錯誤,否則編譯器會報如下錯誤:
Errors thrown from here are not handled
然而如果你不想立即處理而是將閉包的 Error 上拋,你可以像上面所提到的,在 calculate 函數(shù)簽名里加上 throws 來表示這個函數(shù)可能會拋出 Error。
可這樣 calculate 函數(shù)就不樂意了:“我明明不會拋出錯誤,可能拋出錯誤的是我的參數(shù) equation,參數(shù)是外面?zhèn)鬟M來的,這與我何干?”
于是 Swift 使用一個新的關(guān)鍵字 rethrows 來表示這種情況。以后如果在某個函數(shù)聲明中看到 rethrows 就說明這個函數(shù)的閉包或函數(shù)傳參可能拋出錯誤,而這個函數(shù)本身不會拋出錯誤。
defer
上面提到在 do-catch 語句中一旦拋出錯誤其作用域內(nèi)之后的代碼將不再執(zhí)行,但有時候即使拋出錯誤依然希望某些代碼可以執(zhí)行,比如:清理資源、關(guān)閉上下文、打點統(tǒng)計等。
Swift 中提供 defer 語句用于在退出當前作用域之前執(zhí)行指定的代碼:
defer {
// 需要執(zhí)行的代碼
}
在 defer 語句中的語句無論程序是正常推出或者是拋出錯誤而退出都會確保執(zhí)行。
如果多個 defer 語句出現(xiàn)在同一作用域內(nèi),那么它們執(zhí)行的順序與出現(xiàn)的順序相反。給定作用域中的第一個 defer 語句,會在最后執(zhí)行,這意味著代碼中最靠后的 defer 語句中引用的資源可以被其他 defer 語句清理掉:
func someFunc() {
defer { print("First") }
defer { print("Second") }
defer { print("Third") }
}
someFunc()
// 打印“Third”
// 打印“Second”
// 打印“First”
fatalError
有時候你可以確保代碼不會拋出錯誤,如果真的有出乎意料的異常發(fā)生,希望可以讓程序閃退,這時可以嘗試使用 fatalError:
do {
try division(10, 5)
} catch {
fatalError()
}
其實你在之前已經(jīng)看到了上面代碼的等價版本:
try! division(10, 5)