do-catch 与 try/try?/try!
本教程共 93 篇 · 第 45 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:完整掌握 Swift 错误处理闭环——用
do-catch接住错误、try/try?/try!三种调用风格,以及catch如何按错误类型分流。
上一章我们学会抛出错误,但错误抛出来总得有人接。Swift 提供四种应对:传播给上层、用 do-catch 处理、转成可选值、或断言绝不会出错。这一章把这几招讲透。
1-1 try 关键字的作用
调用任何可能抛错的函数,前面必须写 try。try 本身不做处理,它只是个”标记”,告诉读代码的人和编译器”这行可能抛错,请做好准备”。没有 try,编译都过不了。
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
struct Item {
var price: Int
var count: Int
}
struct VendingMachine {
var inventory = [
"Candy Bar": Item(price: 2, count: 5),
"Chip": Item(price: 3, count: 0),
]
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)
}
print("Dispensing \(name)")
}
}
var favoriteSnacks = ["Alice": "Candy Bar", "Bob": "Chip"]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
buyFavoriteSnack 自己标了 throws,所以 vend 抛出的错误会顺着它继续往上传播,由更上层的调用者处理。这就是”传播”——不需要在每层都接住。
1-2 do-catch 捕获错误
真正”就地处理”错误,靠 do-catch。把可能抛错的代码放进 do 块,用 try 标记;错误一旦抛出,就按 catch 子句从上到下匹配,交给能处理的那一条。
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("选错了。")
} catch VendingMachineError.outOfStock {
print("缺货。")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("钱不够,还差 \(coinsNeeded) 枚。")
} catch {
print("未知错误:\(error)。")
}
哪个 catch 先匹配上,就走哪条。do 块里如果没抛错,就正常往下执行 print("Success! Yum.")。最后那个不带任何模式的 catch 是”兜底”,它匹配任意错误,并把错误绑到常量 error 上——这一条不是必需的,但写上能保证所有错误都有归宿。
1-3 catch 的多种匹配写法
catch 后面可以写模式,玩法不少:
- 单个错误:
catch VendingMachineError.outOfStock { ... } - 多个错误用逗号并列:
catch VendingMachineError.invalidSelection, VendingMachineError.outOfStock { ... },任一命中即处理。 - 带
where条件:catch VendingMachineError.insufficientFunds(let n) where n > 5 { ... },只对”差得比较多”的情况特殊处理。 - 用
is判断类型:catch is VendingMachineError { ... }只处理这一类错误,其余放行传播。
如果所有 catch 都没匹配上,错误会继续往外传播。但要注意:错误最终必须被某处处理;若一路传到最顶层还没人接,运行时会崩。
Tip不确定要抓哪几种错误时,先写
catch { print(error) }把错误打出来,看清类型再细化分支。这是调试错误处理的常用起步方式。
1-4 try? 把错误转成可选值
有时候你不在乎错在哪,只关心”成没成”。try? 会把抛错的函数变成返回可选值:成功就返回里面的值,失败(抛错)就返回 nil。
func someThrowingFunction() throws -> Int {
return 42
}
let x = try? someThrowingFunction() // x 是 Int?
它等价于下面这段 do-catch:
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
try? 适合”失败就当没有、别无其他动作”的场景。比如依次尝试多个数据源,谁先成功用谁:
func fetchData() -> String? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
func fetchDataFromDisk() throws -> String { "磁盘数据" }
func fetchDataFromServer() throws -> String { "服务器数据" }
1-5 try! 强制忽略错误
如果你百分之百确定某次调用绝对不会抛错,可以用 try! 关掉错误传播。一旦它真的抛了错,运行时直接崩溃,和强制解包 ! 一样危险。
func loadImage(atPath path: String) throws -> String {
// 真实场景会读取并解码图片文件,这里仅作占位
return path
}
let photo = try! loadImage(atPath: "./Resources/photo.jpg")
只有在”抛错在逻辑上不可能发生”时才用,比如资源已经打包进应用、确定存在。日常开发里,try? 或 do-catch 通常更安全。
Warning
try!和!一样,是把安全检查交给你的判断。能写try?拿到可选、再if let解包的,就别用try!。
1-6 必须被处理与三种 try 的取舍
do-catch 里有个重要但常被忽略的规则:错误必须最终被处理。如果在 do 块里抛出的错误没有被任何 catch 匹配,它会继续向外传播;但传播到最顶层还没人接,运行时就会崩溃。所以写 do-catch 时,要么把所有可能的错误类型都分到对应 catch,要么留一个不带模式的兜底 catch 接住剩余所有。别写”只 catch 一半”的代码,那等于把部分错误偷偷放跑了。
三种 try 风格怎么选,是个常见困惑,给个简单判断:try 配合 do-catch 适合”我需要根据错误类型做不同处理、或想展示具体原因”;try? 适合”成功与否我都只要一个可选结果,不在乎为什么失败”;try! 只在”逻辑上绝对不可能失败”时用,比如资源已确定存在。日常绝大多数情况用 try+do-catch 或 try?,把 try! 当作和 ! 一样的危险通道,能避则避。
还有个细节:do-catch 本身也是个作用域,do 块里用 let 声明的变量,出了 do 块通常就不可见了(除非声明在 do 外面)。所以如果需要把 try 得到的结果在后面继续用,记得把接收变量声明在 do 之前。另外 catch 里拿到的 error 常量,默认类型是 any Error,如果前面用了带具体错误类型的 throws(精化抛出),error 的类型也会相应变具体,可以直接 switch 穷举。
1-7 易错点速记
错误必须最终被处理:没被任何 catch 匹配就继续向外传播,传到顶层还没人接就会崩。所以要么把错误类型都分到对应 catch,要么留一个兜底 catch 接住剩余。
三种 try 的取舍:try+do-catch 适合按错误类型精细处理;try? 适合”只要可选结果、不在乎为何失败”;try! 只在逻辑上绝不可能失败时用,和 ! 一样危险。日常多用前两种。
do-catch 是作用域,要在外面继续用 try 的结果,就把接收变量声明在 do 之前。catch 里的 error 默认是 any Error,用了精化抛出时类型会变具体,可直接 switch 穷举。新手容易写出”只 catch 一半”的代码——以为 catch 写了就不会崩,结果漏掉的错误类型传播到顶层才炸。养成习惯:要么穷举所有 case,要么用无模式 catch 兜底,绝不留半截子处理。
写错误处理时还有一个心智模型值得建立:把”可能失败”当成函数的正常签名的一部分。标了 throws 的函数,调用处就躲不掉 try;正因为有这个标记,编译器逼着你在每一层都表态——要么就地处理,要么继续传播。这种”无法假装错误不存在”的设计,正是 Swift 比很多语言更安全的根源。新手觉得 try/catch 啰嗦,其实啰嗦换来的是:再也没有未被处理的失败悄悄溜到线上。
1-8 小结
错误处理的完整闭环是:throws 标记可能抛错的函数,throw 抛出错误,try 标记调用点;用 do-catch 按错误类型精细处理,用 try? 转成可选值图省事,用 try! 在确信不抛错时关掉检查。四种方式各有适用面,核心原则仍是”别让错误无声地崩掉”。下一章的 defer 是配合这一切的清理利器。