Swift 中的错误处理
将可能遇到的异常尽可能扼杀在编译器是 Swift 在安全性上至始至终贯彻的理念,例如之前提到的可选型以及本文即将讨论的错误处理 (Error Handling)。
错误(Error)
可以简单的将错误划分为编译错误、逻辑错误以及运行时错误
-
编译错误
let a = 10 a = 20 // 编译器报错: Cannot assign to value: 'a' is a 'let' constant
func name(v: Int) -> Int { return "hello" }
// 编译器报错:Cannot convert return expression of type 'String' to return type 'Int'
* 逻辑错误
```swift
let username = "Jay" // 用户名
let password = "123456" // 密码
// 定义登录方法,传入用户名和密码作为参数
func login(userName: String, password: String, completion: (Bool, Error) -> Void) {
// .....
}
// 调用 login 方法时,错误的将 username 和 password 参数传反
login(userName: password, password: userName) { (success, error) in
// ..
}
- 运行时错误
/// 除法运算 /// /// - Parameters: /// - dividend: 被除数 /// - divisor: 除数 /// - Returns: 除法计算结果 func division(_ dividend: Int, _ divisor: Int) -> Int { return dividend / divisor } let result = division(10, 0) print(result) // 运行时报错: Fatal error: Division by zero
编译错误很容易发现,因为压根无法编译通过;逻辑错误稍微隐蔽一些,尤其程序员一旦陷入思维定势即使是很简单的错误都很难被发现,通常需要反复排查以及 Code Review。
运行时错误是最为棘手的,往往这类错误产生的原因种类繁多,比如 iOS 开发中常见的:野指针访问、数组越界、给不存在的方法发送消息...
Swift 推出一套错误处理机制来尝试解决运行时错误,如果使用得当,可以在一定程度上达成将错误扼杀在编译阶段的目的。
错误协议(Error protocol)
在 Swift 标准库中定义了一个名为 Error 的协议:
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 协议来表示错误类型,但出于性能和规范性考虑我们通常只使用遵循 Error 协议的枚举(Enumerations)和结构体(structure)来表示错误:
- 使用枚举定义错误类型
enum DivisionError: Error {
case invalidInput(String)
case overflow(Int, Int)
}
- 使用结构体定义错误类型
struct XMLParsingError: Error {
enum ErrorKind {
case invalidCharacter
case mismatchedTag
case internalError
}
let line: Int
let column: Int
let kind: ErrorKind
}
抛出错误(Throwing errors)
现在已经定义好错误类型,如何才能在我们的代码中使用这些错误呢?
throw、throws、try 关键字
函数内部通过 throw 抛出自定义 Error,可能抛出 Error 的函数必须加上 throws 声明:
// 在参数和返回值之间加上 throws 关键字
func division(_ dividend: Int, _ divisor: Int) throws -> Int {
if divisor == 0 {
// 使用 throw 关键字抛出自定义的 DivisionError 错误
throw DivisionError.invalidInput("0 不能作为除数!")
}
return dividend / divisor
}
在调用可能抛出 Error 的函数时,需要在函数名之前加上 try 关键字:
let result = try division(10, 0)
print(result)
这次报出的错误不再是Fatal error: Division by zero
,而是我们自定义的Fatal error: DivisionError.invalidInput("0 不能作为除数!")
再看苹果官方文档的一个例子:
func parse(_ source: String) throws -> XMLDoc {
// ...
throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
// ...
}
let xmlDoc = try parse(myXMLData)
从上面两个例子不难看出 throws 和 try 总是一起出现的:一旦函数签名中出现 throws 即表示这个函数可能会抛出错误,此时调用这个函数时编译器将强制添加 try 关键词,否则无法编译通过。
但需要注意:使用 try 只是保证编译通过,编译器并没有帮我们自动处理异常,异常的捕获和处理都需要程序员自己进行。
注:使用结构体定义的 XMLParsingError 可以处理的精细程度要比枚举定义的 DivisionError 更高,通常来说枚举类型已经足够用来表达错误类型,因此本文接下来都将使用 DivisionError 来演示错误类型。
错误处理(Handling errors)
在 Swift 中有两种处理错误的方式:
- 使用 do-catch 捕捉 Error
- 不捕捉 Error,在当前函数增加 throws 声明,Error 将自动抛给上层函数
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 {}
内,一旦try division(10, 0)
抛出错误,其作用域内之后的代码将不再执行,即print(result)
不会执行如果需要获取某个 catch 到 Error枚举的关联值,可以参考
let DivisionError.invalidInput(msg)
将 Error 上抛
如果不想立即处理错误,还可以将错误上抛,交给上层的函数处理:
func calculate() throws {
let result = try division(10, 0)
print(result)
}
在调用 try division(10, 0)
的时候不立即处理错误,可以在当前函数 calculate()
加上 throws 关键字,表示将错误抛给 calculate()
函数。
此时调用try calculate()
,编译器依然会提示: Errors thrown from here are not handled
。
将错误上抛只是将错误处理的交给其他函数来处理,最终依然需要使用 do-catch
进行处理:
do {
try calculate()
} catch DivisionError.invalidInput {
print("非法参数")
} catch DivisionError.overflow {
print("越界")
} catch {
print("其他错误")
}
需要注意:如果一直抛到最顶层的 main 函数都不进行处理,编译器不会再进行提醒,而是在运行时直接报错:
Fatal error: Error raised at top level: 错误处理.DivisionError.invalidInput("0 不能作为除数!")
try?、try!
如果你根本不在乎抛出错误的细节,有时我们只是想快速的获取 division(10, 0)
的值,do-catch 的语法就显得有些冗长。幸好 Swift 还提供了一种语法糖来简化整个过程:
var result = try? division(10, 0)
使用 try? 会忽略掉可能抛出异常的细节,并将函数的返回值包装为可选型。因此完全等价于下面的代码:
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()
的结果不会为 nil,try! 的确是个漂亮的语法糖。但往往这样的『确保』是靠不住的,因此要谨慎使用 try!
rethrows
我们知道在 Swift 中,函数是一等公民,可以作为参数或返回值参与到另一个函数中,那如果将一个可能抛出错误的闭包作为参数会怎样呢?
func calculate(_ number1: Int, _ number2: Int, _ equation: (Int, Int) throws -> Int) -> Int {
return try equation(number1, number2)
}
calculate 函数的第三个参数 equation 为一个可能抛出错误的闭包,在函数体内调用 equation 时需要加上 try 关键字,并且处理可能出现的错误,否则编译器会报如下错误:
Errors thrown from here are not handled
然而如果你不想立即处理而是将闭包的 Error 上抛,你可以像上面所提到的,在 calculate 函数签名里加上 throws 来表示这个函数可能会抛出 Error。
可这样 calculate 函数就不乐意了:“我明明不会抛出错误,可能抛出错误的是我的参数 equation,参数是外面传进来的,这与我何干?”
于是 Swift 使用一个新的关键字 rethrows 来表示这种情况。以后如果在某个函数声明中看到 rethrows 就说明这个函数的闭包或函数传参可能抛出错误,而这个函数本身不会抛出错误。
defer
上面提到在 do-catch 语句中一旦抛出错误其作用域内之后的代码将不再执行,但有时候即使抛出错误依然希望某些代码可以执行,比如:清理资源、关闭上下文、打点统计等。
Swift 中提供 defer 语句用于在退出当前作用域之前执行指定的代码:
defer {
// 需要执行的代码
}
在 defer 语句中的语句无论程序是正常推出或者是抛出错误而退出都会确保执行。
如果多个 defer 语句出现在同一作用域内,那么它们执行的顺序与出现的顺序相反。给定作用域中的第一个 defer 语句,会在最后执行,这意味着代码中最靠后的 defer 语句中引用的资源可以被其他 defer 语句清理掉:
func someFunc() {
defer { print("First") }
defer { print("Second") }
defer { print("Third") }
}
someFunc()
// 打印“Third”
// 打印“Second”
// 打印“First”
fatalError
有时候你可以确保代码不会抛出错误,如果真的有出乎意料的异常发生,希望可以让程序闪退,这时可以尝试使用 fatalError:
do {
try division(10, 5)
} catch {
fatalError()
}
其实你在之前已经看到了上面代码的等价版本:
try! division(10, 5)