引入
错误处理是对程序中的错误做出响应并从中恢复的过程。 Swift为运行时抛出、 捕获、 传播和处理可恢复错误提供了一流的支持。
有些操作并不能保证始终完成执行或产生有用的输出。 可选项用于表示没有值, 但当操作失败时, 了解导致失败的原因通常很有用, 这样你的代码就能做出相应的响应。
表示和抛出错误
在Swift中, 错误由符合 Error 协议的类型值表示。 这种空协议表示一种类型可用于错误处理。
Swift枚举尤其适用于对一组相关的错误条件进行建模, 相关值允许传递有关错误性质的附加信息。
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
通过抛出错误, 可以说明发生了意外情况, 正常的执行流程无法继续。 可以使用抛出语句来抛出错误。
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
处理错误
当抛出错误时, 周围的代码必须负责处理错误。
在Swift中, 有四种处理错误的方法。 你可以将错误从函数传播到调用该函数的代码, 使用 do-catch
语句处理错误, 将错误作为可选值处理, 或者断言错误不会发生。
当函数抛出错误时, 会改变程序的流程, 因此快速识别代码中可能抛出错误的地方非常重要。 要识别代码中的这些地方, 在调用可能抛出错误的函数、 方法或初始化器的代码前写入 try
关键字(或 try? )。
使用抛出函数传播错误
要表示函数、 方法或初始化器可以抛出错误, 可以在函数声明中的参数后写入 throws
关键字。 标有 throws
的函数称为抛出函数。
如果函数指定了返回类型,则应在返回箭头 (->) 之前写入 throws 关键字。
抛出函数会将内部抛出的错误传播到调用它的作用域。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
下面的示例, 自动售货机有vend方法, 如果请求物品没有现货, 或者没有足够的钱, 就会抛出 VendingMacheError。
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 vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing \(name)")
}
}
下面的 buyFavoriteSnack函数查找指定人喜欢的零食, 尝试通过vend方法完成购买。 因为vend方法可能出错, 所以调用方式前面需要加上 try 关键字。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
使用 Do-Catch 处理错误
使用 do-catch
语句可以通过运行代码块来处理错误。 如果 do
子句中的代码抛出错误,
就会与 catch
子句进行匹配,以确定哪个子句可以处理该错误。
do {
try <#expression#>
<#statements#>
} catch <#pattern 1#> {
<#statements#>
} catch <#pattern 2#> where <#condition#> {
<#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
<#statements#>
} catch {
<#statements#>
}
在catch后面编写规则,确定可以处理的错误。 如果没有规则, 表示处理任何错误,
并将错误绑定到 error
常量。
示例
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
catch 子句不必处理 do 语句中代码可能抛出的所有错误。 如果没有一个 catch 子句处理错误, 错误就会传播到周围的作用域。 但是传播的错误必须由某个周围作用域处理。 在非抛出函数中, 外层 do-catch 语句必须处理该错误。 在抛出函数中, 外层 do-catch 语句或调用者必须 处理该错误。 如果错误没有得到处理就传播到顶层作用域, 就会产生运行时错误。
如果想要通知捕获多个错误, 可以列出多个错误, 并使用逗号分隔。
func eat(item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
print("Invalid selection, out of stock, or not enough money.")
}
}
将错误转化成可选值
使用 try?
可以将错误转换为可选值, 从而处理错误。 如果在评估 try?
表达式时出错,
表达式的值将为 nil。
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
想以同样的方式处理所有错误时, 使用 try?可以编写简洁的错误处理代码。
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
禁用错误传播
有时知道一个抛出函数或方法实际上不会在运行时抛出错误。 在这种情况下, 你可以在表达式前写入
try!
以禁用错误传播, 并在运行时断言不会抛出错误的情况下进行调用。 如果确实发生了错误,
则会出现运行时错误。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定错误类型
上述所有示例都使用了最常见的错误处理方式, 即代码抛出的错误可以是符合错误协议的任何类型的值。 这种方法符合实际情况, 即你不可能提前知道代码运行时可能发生的所有错误, 尤其是在传播其他地方 抛出的错误时。 它还反映了一个事实, 即错误会随着时间的推移而改变。 新版本的库,包括你的依赖 库使用的库, 可能会产生新的错误, 而现实世界中用户配置的复杂性可能会暴露出开发或测试过程中 不可见的故障模式。 上述示例中的错误处理代码总是包含一个默认情况, 用于处理没有特定 catch 子句的错误。
大多数Swift代码都不会指定所抛出错误的类型。 不过以下特殊情况, 你可能会限制代码只抛出一种 特定类型的错误:
- 在不支持动态分配内存的嵌入式系统上运行代码时。
- 当错误是某个代码单元(如库)的实现细节,而不是该代码接口的一部分时。
- 在只传播由通用参数描述的错误的代码中, 例如一个接收闭包参数的函数, 会传播来自该闭包的任何错误。
有这样一个错误类型。
enum StatisticsError: Error {
case noRatings
case invalidRating(Int)
}
要指定函数只抛出 StatisticsError 值作为其错误, 可以在声明函数时写入 throws(StatisticsError), 而不是只写入 throws。 这种语法也称为类型化抛出, 因为在声明中的 throws 后写入了错误类型。
func summarize(_ ratings: [Int]) throws(StatisticsError) {
guard !ratings.isEmpty else { throw .noRatings }
var counts = [1: 0, 2: 0, 3: 0]
for rating in ratings {
guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
counts[rating]! += 1
}
print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}
除了指定函数的错误类型外, 还可以为 do-catch 语句编写特定类型错误。
let ratings = []
do throws(StatisticsError) {
try summarize(ratings)
} catch {
switch error {
case .noRatings:
print("No ratings available")
case .invalidRating(let rating):
print("Invalid rating: \(rating)")
}
}
// Prints "No ratings available"
指定清理操作
使用延迟语句可以在代码执行离开当前代码块之前执行一组语句。 无论代码执行以何种方式离开当前代 码块—是由于抛出错误还是由于 return 或 break 等语句—该语句都可以让你执行任何必要的清理 工作。
延迟语句延迟执行,直到退出当前作用域。 该语句由 defer 关键字和稍后执行的语句组成。 延迟语句不得包含任何会将控制权转出语句的代码, 如中断或返回语句, 或抛出错误。 延迟操 作的执行顺序与源代码中的顺序相反。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
上例使用了延迟语句, 以确保 open 函数有相应的 close 调用。