11-4.【错误处理】在设计 API 时,什么时候应该用 throws,什么时候应该用返回 Optional/Result?

6 阅读3分钟

在设计 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 letswitch

使用 Result<T, E>:异步与存储的利器

由于 throws 是一种函数执行行为,它无法被“保存”起来。

  • 典型场景

    • 异步回调:在 Swift Concurrency(async/await)出现之前,逃逸闭包必须使用 Result
    • 状态存储:如果你需要把一个操作的结果存起来,稍后再处理,Result 是唯一选择。

3. 性能维度:高频 vs. 低频

根据我们之前讨论的底层原理:

  • Optional 的成本最低:因为它只是一个简单的枚举位检查。
  • throws 的成功成本极低,但失败成本较高:涉及错误对象的装箱和类型查找。
  • Result 的成本恒定:无论成功还是失败,都需要进行枚举包装。
频率失败概率推荐方案
高频循环经常失败Optional (避免 throw 的装箱开销)
常规业务偶尔失败throws (语法最简洁,成功路径最快)
并发任务流需手动传递错误Result (方便在不同线程间传递状态)

4. 决策矩阵表

场景描述推荐方案示例代码
简单的查找/筛选Optionalfunc findUser(id: String) -> User?
复杂的初始化或处理throwsfunc loadConfiguration() throws -> Config
作为闭包参数或变量存储Resultvar lastResult: Result<Data, Error>
如果不处理错误,程序无法继续throwsfunc saveToDatabase(_ data: Data) throws
Swift 6 嵌入式或极致性能需求Typed Throwsfunc fastOp() throws(MyError) -> Int

总结:防御式设计的金律

  1. 优先考虑 Optional:如果 nil 的含义不言自明。
  2. 默认选择 throws:对于任何可能因为多种逻辑原因失败的任务。
  3. 万不得已选 Result:仅当你需要存储结果,或者正在编写不支持 async/await 的老旧异步代码时。