错误处理:Error 协议与 throw
本教程共 93 篇 · 第 44 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:理解错误处理的定位——表达”出错了且知道为什么”,学会用枚举建模错误、用 throw 抛出、用 throws 标记函数。
可选类型只回答了”有没有值”,但很多失败场景你更想知道”为什么失败”。比如读文件,可能”文件不存在""没权限""格式不对”。光返回 nil 信息量太少。Swift 的错误处理就是为这类情况准备的:它让你抛出携带原因的错误,并在调用处明确处理。
1-1 错误处理和可选类型的区别
可选类型适合”缺失即足够说明问题”的场合,比如查字典没找到就 nil。但错误往往分很多种,每种需要不同应对。错误处理能区分错误类型,让程序针对性地恢复或提示用户。两者不是替代关系,而是各管一摊:简单的不存在用可选,复杂的失败原因用错误。
Swift 的错误处理和别的语言异常捕获很像,都用 try、catch、throw 这些关键字。但有个关键差别:Swift 的 throw 不会像某些语言那样去”展开调用栈”,开销和一次函数返回差不多,所以性能上很轻。
1-2 Error 协议
在 Swift 里,凡是遵循 Error 协议的类型,都能用来表示错误。Error 是个空协议,本身没有任何要求,它的作用就是”打一个标记”,告诉编译器”这个类型可以用来做错误处理”。
任何类型都能遵循 Error,但实践中最常用的是枚举,因为一组相关的错误天然适合用枚举的多个 case 来表达。
1-3 用枚举建模错误
枚举特别适合把”一组相关的失败情况”列清楚。举个自动售货机的例子:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
三个 case 分别是”选了不存在的商品""钱不够""缺货”。其中 insufficientFunds 还带了一个关联值 coinsNeeded,表示”还差多少枚硬币”。关联值让错误能携带具体细节,调用方拿到错误后能精确知道该怎么补救。
Tip用枚举建模错误时,把”需要让调用方知道的量”放进关联值,比另外定义一堆字段清爽。比如”钱不够”如果不带差多少,调用方还得自己去算,体验就差了。
1-4 用 throw 抛出错误
throw 用来抛出一个错误值,表示”出意外了,正常流程走不下去了”。抛出的必须是一个遵循 Error 的值:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
一旦执行到 throw,当前这段代码的执行就立刻中断,控制流跳到能处理这个错误的地方(下一章讲的 do-catch 或函数的调用者)。throw 和 return 类似,都会让函数本次调用提前结束,只是 throw 带的是”失败信号”。
1-5 用 throws 标记函数
如果一个函数、方法或初始化器内部可能抛出错误,必须在声明里写 throws。写了 throws 的函数叫”抛出函数”(throwing function)。如果函数有返回类型,throws 写在返回箭头 -> 前面:
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
只有抛出函数才能把内部错误往上传播;非抛出函数里抛出的错误,必须在函数内部自己处理掉。看一个会抛错的函数:
struct Item {
var price: Int
var count: Int
}
func vend(itemNamed name: String, inventory: [String: Item], coinsDeposited: Int) 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)
}
print("Dispensing \(name)")
}
这里一连串 guard 检查,任意一条不满足就 throw 对应的错误并立刻退出。只有全部通过,才会真正”出货”。因为 vend 标了 throws,它抛出的错误会传给调用者,由调用者决定怎么处理——这正是下一章的主题。
Note抛错会打断正常流程,所以别拿错误当普通的控制流用。错误应该留给”真正的意外”,日常的分支判断还是用
if/else或switch更合适。
1-6 建模错误与抛出处理解耦
用枚举建模错误时,有几个实用技巧。第一,错误 case 的命名要”对用户或调用者有意义”,而不是只描述内部实现。比如 insufficientFunds 比 negativeBalance 更贴近”这次操作为什么失败”。第二,需要携带细节的就用关联值,比如差多少枚硬币、哪个字段不合法;不需要细节的纯 case 就保持简单。第三,如果错误要展示给用户看,可以让枚举遵循 CustomStringConvertible,给每个 case 提供一个本地化的中文描述,调用方拿到错误后直接拿来显示。
标准库里 Error 协议本身很轻,但 Swift 也允许错误带更多能力。例如,遵循 LocalizedError 协议可以提供 errorDescription,遵循 CustomNSError 可以对接更底层的错误域。这些在你未来对接某些框架时有用,但初学阶段,用普通枚举加关联值已经能覆盖绝大多数业务场景,不必一上来就堆协议。
还有一点容易误解:throw 出来的错误,并不是只能被 catch 接住才算处理。你也可以用 try? 把它吞成 nil(下一章会讲),或者在 throws 函数之间一路传播。也就是说,抛出错误只是”声明这里可能失败”,至于失败由谁、以何种方式应对,是调用方的自由。这种”抛出与处理解耦”的设计,让底层函数专注业务逻辑,上层统一决定怎么兜底,职责清晰。
1-7 易错点速记
建模错误时,让 case 名对调用者有意义,需要细节就用关联值携带。抛错只是”声明这里可能失败”,怎么应对是调用方的自由——可以 do-catch 精细处理,可以 try? 转成 nil,也可以一路 throws 传播。不要把 throw 当普通控制流,它只留给真正的意外。遵循 Error 的普通枚举加关联值,已能覆盖绝大多数业务场景。
如果你让错误要展示给用户,遵循 LocalizedError 提供描述即可,初学不必一上来就堆协议。重点是先把”哪些情况算失败、各携带什么信息”想清楚,枚举的 case 设计好了,上层处理自然顺。
1-8 小结
错误处理用遵循 Error 协议的类型(通常是枚举)建模失败原因,用 throw 抛出,throws 标记可能抛错的函数。它比可选类型信息更丰富,适合”失败有很多种、要分别应对”的场景。错误抛出来之后,由谁、怎么接住?下一章讲 do-catch 与 try 系列。