理解难度
★★★★☆
实用程度
★★★☆☆
代码运行中,有时会遇到错误需要处理,例如当读取一个档案,但是档案可能不存在或是没有读取权限;或是一个购物车需要进行业务逻辑上的判断,结帐前要检查是否有商品或是超过数量库存等等。对于错误的抛出、捕获、传递及处理, Swift 都提供了完整的支持。
错误的描述与抛出
首先我们必须定义一组错误描述,来让代码中遇到错误时,可以清楚知道当前是遇到了什么错误状况,以及各自匹配后续的处理。Swift 中通常是使用一个遵循Error协定(protocol)的枚举来表示一组错误描述(Error是一个空的协定,只是为了告诉 Swift 这个枚举是用来表示错误描述)。
下面的例子是一个自动贩卖机定义的一组错误描述的枚举,成员依序为:
- 无此商品
- 金额不足(有一个相关值为还需要补足多少钱币)
- 商品已卖光
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
当遇到一个错误的时候,这里会表示有一个错误发生,然后将这个错误抛出,后续再由自己定义处理方式或交由 Swift 自动处理。Swift 中使用关键字throw来抛出一个错误。下面例子表示抛出一个自动贩卖机还需要补足 3 个钱币的错误:
throw VendingMachineError.insufficientFunds(coinsNeeded: 3)
使用抛出函数传递错误
前面说明了如何抛出错误,接着我们必须设计一个函数,其内部会经过逻辑判断是否发生异常或错误,当发生时便会抛出错误。Swift 使用关键字throws来标记函数,称其为抛出函数(throwing function),格式如下:
func 函数名称() throws -> 返回值型别 {
内部执行的代码
}
抛出函数中的抛出(throw)有点类似return,因为抛出一个错误表示一个异常或错误发生了,所以正常的执行流程会立即中止,其后的代码都不会继续执行,会直接传递至处理错误的地方继续。
只有抛出函数可以传递错误。任何在一个非抛出函数中抛出的错误都必须在该函数内部处理。
下面就定义一个自动贩卖机的类别:
// 先定义一个结构体来表示一个商品的内容 分别为商品的价钱及数量
struct Item {
var price: Int
var count: Int
}
// 定义一个自动贩卖机的类别
class VendingMachine {
// 自动贩卖机内的商品
var inventory = [
"可乐": Item(price: 25, count: 4),
"洋芋片": Item(price: 20, count: 7),
"巧克力": Item(price: 35, count: 11)
]
// 目前已投入了多少钱币 预设值为 0
var coinsDeposited = 0
// 所有判断错误的逻辑都通过后 确定购买商品的动作
func dispenseSnack(snack: String) {
print("Dispensing \(snack)")
}
// 贩售的动作 确定售出前会做些判断
// 这是一个抛出函数 所以函数名称需要加上 throws
func vend(itemNamed name: String) throws {
// 检查是否有这个商品 没有的话会抛出错误
guard var 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
item.count -= 1
inventory[name] = item
dispenseSnack(snack: name)
}
}
错误的捕获及处理
前面定义的类别中有一个抛出函数,如果遇到错误时会将错误抛出并传递至错误处理的地方,目前尚未定义怎么处理错误,所以这时 Swift 会自动处理,不过这可能就是意味着代码中止,所以我们还是自行定义错误处理的方式。
Swift 使用 do-catch 语句来定义错误的捕获(catch)及处理,每一个catch表示可以捕获到一个错误抛出的处理方式,下面就是格式:
do {
try 抛出函数
其他执行的代码
} catch 错误1 {
处理错误1
} catch 错误2 {
处理错误2
}
- 如果要呼叫抛出函数,必须在函数前加上 try 关键字。
- 如果要捕获抛出的错误,必须将抛出函数(或抛出错误的代码)写在关键字do包含的大括号{ }内。
- 使用关键字 catch 来匹配要捕获的每个错误。
下面是一个例子:
// 生成一个自动贩卖机类别的实例 并设置已投入 8 个钱币
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
// 进行错误的抛出、捕获及处理
do {
// 呼叫抛出函数 我要购买可乐这个商品
try vendingMachine.vend(itemNamed: "可乐")
// 其他可能需要执行的代码 这边先省略
// 下面就每个 catch 为各自匹配错误的处理
} catch VendingMachineError.invalidSelection {
print("无此商品")
} catch VendingMachineError.outOfStock {
print("商品已卖光")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("金额不足,还差 \(coinsNeeded) 个钱币")
}
上面的代码中可以看到 vendingMachine.vend(itemNamed:) 函数内,每个会抛出的错误都可以在 do-catch 中列出的 catch 语句中匹配到。
而上面的这个例子依序判断错误到最后,会因为钱币不足而抛出 VendingMachineError.insufficientFunds 这个错误,并在外面的 do-catch 中被捕获到,最后会打印出:金额不足,还差 17 个钱币。当然因为已经抛出错误,其后的代码都不会继续执行。
转换错误为可选值
前面提到可以使用一个抛出函数来抛出错误,并使用do-catch来捕获并处理错误。而如果只是要简单让错误发生时,返回为一个nil,像是如下的表示:
// 定义一个抛出函数 会返回一个 Int
func someThrowingFunction() throws -> Int {
// 内部执行的代码
// 假设返回 10
return 10
}
// 声明一个可选型别 Int? 的常量 x
let x: Int?
do {
// 呼叫抛出函数 会返回一个 Int
x = try someThrowingFunction()
} catch {
// 错误发生而被抛出 进而捕获时 将其设为 nil
x = nil
}
如上面的代码功能,我们可以简单的将 try 改成使用 try? ,这样当错误发生要被抛出时,会简单的返回一个nil。如下:
let y = try? someThrowingFunction()
不论原本抛出函数返回的是不是可选值,使用 try? 呼叫的抛出函数,都会返回可选值。
禁用错误传递
当你知道一个抛出函数确定不会在执行时抛出错误,这时可以使用 try! 来呼叫抛出函数,这样会将错误传递禁用,但当错误真的被抛出时,会发生代码运行时错误。
也就是说,使用 try! 呼叫抛出函数来告诉 Swift 确定这个呼叫不会发生异常或错误。还有一点,使用 try! 呼叫抛出函数,可以不用放在 do 的大括号{ }内。
let z = try! someThrowingFunction()
必定执行的代码区块
我们可以使用 defer 定义一个代码区块,当在无论是抛出(throw)错误,或是使用 return、break 结束这个函数后,都必定会执行这个代码区块。
当在需要做清理工作或是释放记忆体之类的代码时很好用,像是一个开启档案的函数。
func someMethod() throws {
// 打开一个资源 像是开启一个档案
defer {
// 释放资源记忆体或清理工作
// 像是关闭一个开启的档案
}
// 错误处理 像是档案不存在或没有读取权限
// 及其他要执行的代码
}
按照上面的代码,这样不论在正常执行代码到最后或是因为发生错误抛出而中止,最后都会执行 defer 内的代码,保证清理工作一定会执行。
如果定义多个 defer ,会先执行最后一个定义的 defer ,再依序往前执行到第一个。
defer 不是一定要与错误处理一起使用,普通的函数内也可以使用。