Swift 错误处理
程序总会遇到"非预期状况"——文件不存在、网络超时、JSON 格式错、用户余额不足。Swift 把错误当成一等公民:用 Error 协议描述错误,用 throws 标记可能失败的函数,用 do-catch 捕获处理。这套机制比 Java 的 checked exception 更轻量,又比 Python 的 try/except 更类型安全。本章讲透。
1. 定义错误与 throwing 函数
Swift 的错误本质是采纳了 Error 协议的任何类型。绝大多数情况用枚举——每种 case 代表一种错误,还能带关联值:
// 1. 定义错误:任何采纳 Error 协议的类型都能 throw
enum VendingError: Error {
case outOfStock // 无库存
case insufficientFunds(coinsNeeded: Int) // 带关联值
case invalidSelection
}
// 2. throwing 函数:声明可能抛错(用 throws)
struct Item { var price: Int; var count: Int }
class VendingMachine {
private var inventory = [
"Candy": Item(price: 3, count: 5),
"Chips": Item(price: 5, count: 0),
]
var coinsDeposited = 0
// throws 关键字:函数可能抛错
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingError.invalidSelection // 抛错
}
guard item.count > 0 else {
throw VendingError.outOfStock
}
guard coinsDeposited >= item.price else {
throw VendingError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
inventory[name]?.count -= 1
print("出货: \(name)")
}
}关键语法:
- Error 协议:空协议,任何类型采纳后即可 throw。
- throws 关键字:函数声明里写
throws,表示它可能抛错。返回类型写在 throws 之后,如func foo() throws -> Int。 - throw 语句:抛出一个错误,立即退出函数。
2. 调用:do-catch 与三种 try
调用 throwing 函数,必须用 try 前缀(让阅读者一眼看出"这行可能失败"),并用 do-catch 包起来捕获错误:
// 调用 throwing 函数:必须用 try,且要包在 do-catch 里
let machine = VendingMachine()
machine.coinsDeposited = 10
do {
try machine.vend(itemNamed: "Candy") // try 标记"这里可能抛错"
print("购买成功")
} catch VendingError.outOfStock {
print("没货了")
} catch VendingError.insufficientFunds(let needed) {
print("钱不够,还差 \(needed) 个硬币")
} catch {
// catch 后不带模式:匹配所有剩余错误,error 是隐式常量
print("其它错误: \(error)")
}
// 三种 try
let x = try? machine.vend(itemNamed: "Chips") // 出错返回 nil(可选)
let y = try! machine.vend(itemNamed: "Candy") // 出错直接崩溃(确信不会错时)三种 try 的取舍:
- try:标准形式,必须配合 do-catch 或在 throwing 函数里。
- try?:出错返回 nil,成功返回可选值。适合"我不在乎具体错误,只关心成没成"。
- try!:出错直接崩溃(断言)。只在你确信不会失败时用(如写死的本地资源),否则生产环境会闪退。
3. defer:保证资源清理
defer 块里的代码,无论函数怎么退出——正常返回、抛错、提前 return——都会执行。这是处理文件句柄、锁、数据库连接的最佳工具:
// defer:无论怎么退出函数(正常返回/抛错/return)都会执行的清理代码
func processFile(_ path: String) throws {
let handle = openFile(path) // 假设打开资源
defer {
closeFile(handle) // 保证一定关闭
}
let data = try readBytes(handle)
if data.isEmpty {
throw FileError.empty // 即使抛错,defer 仍执行 → close
}
print("处理完成,\(data.count) 字节")
// 函数结束,defer 自动 close
}
// 多个 defer:LIFO(后进先出)
func example() {
defer { print("1") }
defer { print("2") }
defer { print("3") }
print("函数体")
}
example() // 函数体 → 3 → 2 → 1多个 defer 按LIFO(后进先出)执行,像栈一样。建议:每打开一个资源就紧跟一个 defer,代码更清晰。
4. Result 类型:错误当值传
Swift 5 引入 Result<Success, Failure> 枚举,把"成功值或错误"打包成一个值。它特别适合异步回调——传统闭包签名 (data?, error?) 有个尴尬:两个都可能为 nil 也可能都不为 nil。Result 把这种"二选一"关系固化在类型里:
// Result<Success, Failure>:把"可能失败的结果"打包成值
// 适合回调(异步)、链式处理、把错误当数据传递
enum LoadError: Error { case network, parse }
func loadUser(_ id: String) -> Result<String, LoadError> {
if id.isEmpty { return .failure(.network) }
if id == "bad" { return .failure(.parse) }
return .success("用户:\(id)")
}
// 调用方用 switch 处理
switch loadUser("123") {
case .success(let user): print(user)
case .failure(let err): print(err)
}
// 或用 try? 把 Result 转成可选
let value = try? loadUser("123").get() // .get() 在 failure 时 throw
// 异步回调的标准签名(URLSession 旧 API 早期就长这样)
func fetch(_ completion: (Result<String, LoadError>) -> Void) {
completion(.success("OK"))
}Result vs throws 的选择:
- 同步代码:优先 throws + do-catch,语法更短。
- 异步回调:Result 更合适(闭包不能 throw 给调用方)。
- async/await:Swift 5.5+ 直接用 throws,Result 用得越来越少。
5. 错误的传递
throwing 函数里的 try 如果出错,错误会自动向上冒泡,直到被某个 do-catch 捕获。catch 也可以只处理部分错误,剩下的继续传递:
// 错误向上传递:rethrows 与转发
func runTasks(_ tasks: [() throws -> Void]) rethrows {
for task in tasks {
try task() // 转发:tasks 不抛 → runTasks 不抛
}
}
// throwing 闭包,内层错自动冒泡
func loadAndDecode() throws -> User {
let data = try loadData() // 错会自动传给 loadAndDecode 调用方
let user = try decode(data)
return user
}
// do-catch 不一定要处理所有错误,可以让它继续冒泡
func safeLoad() throws {
do {
let x = try riskyOp()
print(x)
} catch is CancellationError {
// 只处理这一种,其它继续抛
print("被取消")
}
// 其它错误自动传给 safeLoad 的调用方
}rethrows 是个特殊修饰:函数参数里有 throwing 闭包,但函数本身不主动抛错——这种函数标 rethrows,只在闭包抛错时才"转发"错误。标准库的 map、filter 都是 rethrows。
6. 错误处理的取舍
| 方式 | 用在哪 | 风险 |
|---|---|---|
| try! | 确信不会失败(本地常量) | 出错即崩溃 |
| try? | 只关心成功/失败 | 丢失错误信息 |
| do-catch | 需要根据错误分支 | 代码长 |
| Result | 异步回调、错误当值 | 样板代码多 |
| assert/fatalError | 程序员错误(不变量违反) | 不该用于业务错误 |
原则:可预期的业务错误(用户输入、网络、文件)用 throws;程序员错误(数组越界、不变量违反)用 precondition/fatalError 让它崩溃,因为这是 bug 不是业务流程。
常见陷阱
- catch 太宽:一个空
catch(只打印error不做分支)把所有错误吞了,排查问题极痛苦。尽量按具体 case 分支。 - 滥用 try!:线上接口用
try!,服务器一改字段就闪退。 - defer 忘写:打开文件没 defer close,抛错后句柄泄漏。
- Error 不带信息:只写
case failed不带关联值,出错后无法知道哪里、为什么失败。带上足够上下文。 - 异步还用 throws 闭包:闭包
() throws -> Void无法跨异步边界传错,改用 Result 或 async/await。
小结
Swift 错误处理 = Error 协议 + throws/throw + do-catch + defer + try?/! + Result。掌握它,你就能写出既健壮又可读的失败处理逻辑——这是从"能跑的代码"到"上线的代码"的关键一步。这是 Swift 系列的最后一篇技术正文,下一篇是进阶与下一步,带你走向 SwiftUI 与真实 App 开发。
← 上一篇 Swift 闭包
下一篇 Swift 进阶与下一步 →