错误处理
错误处理(Error handling)是响应错误以及从错误中恢复的过程。Swift提供了在运行时对可恢复错误的抛出、捕获、传递和操作的一流支持。
某些操作无法保证总是执行完所有代码或总是生成有用的结果。可选类型可用来表示值缺失
,但是当某个操作失败时,最好能得知失败的原因,从而可以作出相应的应对。
举个例子,假如有个从磁盘上的某个文件读取数据并进行处理的任务,该任务会有多种可能失败的情况,包括指定路径下文件并不存在,文件不具有可读权限,或者文件编码格式不兼容。区分这些不同的失败情况可以让程序解决并处理某些错误,然后把它解决不了的错误报告给用户。
注意,Swift中的错误处理涉及到错误处理模式
,这会用到Cocoa和Objective-C中的NSError
。
表示并抛出错误
在Swift中,错误用符合ErrorType
协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。
Swift的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。
抛出一个错误可以让你表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用throws
关键字。
/** * 一个游戏中操作自动贩卖机时可能会出现的错误状态 */
enum VendingMachineError: ErrorType {
case InvalidSelection // 选择无效
case InsufficientFunds(coinsInNeed: Int) // 金额不足
case OutOfStock // 缺货
}
处理错误
某个错误被抛出时,附近的某部分代码必须负责处理这个错误,例如纠正这个问题、尝试另外一种方式、或是向用户报告错误。
Swift中有4
种处理错误的方式。
当一个函数抛出一个错误时,你的程序流程会发生改变,所以重要的是你能迅速识别代码中会抛出错误的地方。为了标识出这些地方,在调用一个能抛出错误的函数、方法或者构造器之前,加上try
关键字,或者try?
或try!
这种变体。
注意,Swift中的错误处理和其他语言中用try
,catch
和throw
进行异常处理很像。和其他语言中(包括 Objective-C )的异常处理不同的是,Swift中的错误处理并不涉及解除调用栈,这是一个计算代价高昂的过程。就此而言,throw语句的性能特性是可以和return语句相媲美的。
用throwing函数传递错误
为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数列表之后加上throws
关键字。一个标有throws
关键字的函数被称作throwing函数
。如果这个函数指明了返回值类型,throws关键词需要写在箭头(->
)的前面。
一个throwing函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。
只有throwing函数可以传递错误。任何在某个非throwing函数内部抛出的错误只能在函数内部处理。
/** * 一个游戏中操作自动贩卖机时可能会出现的错误状态 */
enum VendingMachineError: ErrorType {
case InvalidSelection // 选择无效
case InsufficientFunds(coinsInNeed: Int) // 金额不足
case OutOfStock // 缺货
}
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12,count: 7),"Chips": Item(price: 10,count: 4),"Pretzels": Item(price: 7,count: 11)
]
var coinsDeposited = 0
func dispenseSnack(snack: String) {
print("didpensing \(snack)")
}
func vend(itemName name: String) throws {
guard var item = inventory[name] else { // 对于guard来说,任何使用了可选绑定作为条件的一部分并被分配了值的变量或常量对于剩下的保护语句出现的代码段是可用的。
throw VendingMachineError.InvalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.OutOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.InsufficientFunds(coinsInNeed: item.price - coinsDeposited)
}
coinsDeposited -= item.price
--item.count
inventory[name] = item
dispenseSnack(name)
}
}
let favoriteSnacks = [
"Alice": "Chips","Bob": "Licorice","Eve": "Pretzels",]
func buyFavoriteSnacks(person: String,machine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try machine.vend(itemName: snackName) // 继续传递错误
}
用do-catch处理错误
可以使用一个do-catch
语句运行一段闭包代码来处理错误。如果在do
子句中的代码抛出了一个错误,这个错误会与catch
子句做匹配,从而决定哪条子句能处理它。
在catch
后面写一个匹配模式来表明这个子句能处理什么样的错误。如果一条catch
子句没有指定匹配模式,那么这条子句可以匹配任何错误,并且把错误绑定到一个名字为error
的局部常量。
catch
子句不必将do
子句中的代码所抛出的每一个可能的错误都作处理。如果所有catch
子句都未处理错误,错误就会传递到周围的作用域。然而,错误还是必须要被某个周围的作用域处理的——要么是一个外围的do-catch
错误处理语句,要么是一个throwing
函数的内部。
如果错误被抛出,相应的执行会马上转移到catch
子句中,并判断这个错误是否要被继续传递下去。如果没有错误抛出,do
子句中余下的语句就会被执行。
/** * 一个游戏中操作自动贩卖机时可能会出现的错误状态 */
enum VendingMachineError: ErrorType {
case InvalidSelection // 选择无效
case InsufficientFunds(coinsInNeed: Int) // 金额不足
case OutOfStock // 缺货
}
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12,machine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try machine.vend(itemName: snackName) // 继续传递错误
}
var machine = VendingMachine()
machine.coinsDeposited = 8
do {
try buyFavoriteSnacks("Alice",machine: machine)
} catch VendingMachineError.InvalidSelection {
print("invalid selection")
} catch VendingMachineError.OutOfStock {
print("out of stock")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
print("insuffcient funds,please insert \(coinsNeeded) coins") // insuffcient funds,please insert 2 coins
}
把错误转换成可选值
可以使用try?
通过将错误转换成一个可选值来处理错误。如果在评估try?
表达式时一个错误被抛出,那么表达式的值就是nil
。
enum IntError: ErrorType {
case ZeroError
}
func fun(x: Int) throws -> Int {
if x == 0 {
throw IntError.ZeroError
}
return x
}
let x = try? fun(0)
print(x) // nil
let y: Int?
do {
y = try fun(0)
} catch IntError.ZeroError {
y = nil
}
print(y) // nil
如果你想对所有的错误都采用同样的方式来处理,用try?
就可以让你写出简洁的错误处理代码。
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
禁用错误传递
有时你知道某个throwing
函数实际上在运行时是不会抛出错误的,在这种情况下,你可以在表达式前面写try!
来禁用错误传递,这会把调用包装在一个断言不会有错误抛出的运行时断言中。如果实际上抛出了错误,你会得到一个运行时错误。
enum IntError: ErrorType {
case ZeroError
}
func fun(x: Int) throws -> Int {
if x == 0 {
throw IntError.ZeroError
}
return x
}
// let x = try! fun(0) // 运行时错误:fatal error: 'try!' expression unexpectedly raised an error: IntError.ZeroError: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-700.0.59/src/swift/stdlib/public/core/ErrorType.swift,line 50
指定清理操作
可以使用defer
语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,还是由于诸如return
或者break
的语句。例如,你可以用defer
语句来确保文件描述符得以关闭,以及手动分配的内存得以释放。
defer
语句将代码的执行延迟到当前的作用域退出之前。该语句由defer
关键字和要被延迟执行的语句组成。延迟执行的语句不能包含任何控制转移语句,例如break
或是return
语句,或是抛出一个错误。延迟执行的操作会按照它们被指定时的顺序的相反顺序执行——也就是说,第一条defer
语句中的代码会在第二条defer
语句中的代码被执行之后才执行,以此类推。
即使没有涉及到错误处理,你也可以使用defer
语句。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// 处理文件。
}
// close(file) 会在这里被调用,即作用域的最后。
}
}