在设计 Swift API 时,选择哪种错误处理方式决定了调用方的开发体验(DX)和程序的健壮性。这不仅是一个技术问题,更是一个语义设计问题。
可以根据以下三个维度进行决策:
1. 语义维度:缺失 vs. 故障
使用 Optional<T>:表达“合法的缺失”
当“没有结果”是程序运行中的一种预期内、且原因单一的状态时,使用 Optional。
-
特征:调用方不需要知道“为什么没有”,只需要知道“有没有”。
-
典型场景:
- 在字典中查找一个 Key。
- 通过 ID 在数组中查找对象。
- 转换字符串为整数(
Int("abc"))。
哲学:如果
nil并不意味着“出了错”,而仅仅是“没找到”,请用 Optional。
使用 throws:表达“异常的故障”
当“没有结果”是因为操作失败,且调用方可能需要处理不同的失败原因时,使用 throws。
-
特征:失败包含上下文信息(错误类型),且通常需要调用方采取补救措施。
-
典型场景:
- 文件读取失败(权限不足 vs. 文件不存在)。
- 网络请求失败(超时 vs. 无连接)。
- 数据解析失败。
哲学:如果失败是“意外”或“错误”,且需要解释原因,请用
throws。
2. 交互维度:同步 vs. 异步
使用 throws:同步流的统治者
throws 在同步代码中非常强大,因为它支持自动冒泡(Propagation)和短路逻辑。
- 它能让复杂的逻辑保持线性,不需要嵌套的
if let或switch。
使用 Result<T, E>:异步与存储的利器
由于 throws 是一种函数执行行为,它无法被“保存”起来。
-
典型场景:
- 异步回调:在 Swift Concurrency(async/await)出现之前,逃逸闭包必须使用
Result。 - 状态存储:如果你需要把一个操作的结果存起来,稍后再处理,
Result是唯一选择。
- 异步回调:在 Swift Concurrency(async/await)出现之前,逃逸闭包必须使用
3. 性能维度:高频 vs. 低频
根据我们之前讨论的底层原理:
Optional的成本最低:因为它只是一个简单的枚举位检查。throws的成功成本极低,但失败成本较高:涉及错误对象的装箱和类型查找。Result的成本恒定:无论成功还是失败,都需要进行枚举包装。
| 频率 | 失败概率 | 推荐方案 |
|---|---|---|
| 高频循环 | 经常失败 | Optional (避免 throw 的装箱开销) |
| 常规业务 | 偶尔失败 | throws (语法最简洁,成功路径最快) |
| 并发任务流 | 需手动传递错误 | Result (方便在不同线程间传递状态) |
4. 决策矩阵表
| 场景描述 | 推荐方案 | 示例代码 |
|---|---|---|
| 简单的查找/筛选 | Optional | func findUser(id: String) -> User? |
| 复杂的初始化或处理 | throws | func loadConfiguration() throws -> Config |
| 作为闭包参数或变量存储 | Result | var lastResult: Result<Data, Error> |
| 如果不处理错误,程序无法继续 | throws | func saveToDatabase(_ data: Data) throws |
| Swift 6 嵌入式或极致性能需求 | Typed Throws | func fastOp() throws(MyError) -> Int |
总结:防御式设计的金律
- 优先考虑 Optional:如果
nil的含义不言自明。 - 默认选择 throws:对于任何可能因为多种逻辑原因失败的任务。
- 万不得已选 Result:仅当你需要存储结果,或者正在编写不支持
async/await的老旧异步代码时。