Swift 与 Objective-C

[Swift 基础 #5] throws、try、Result 的选择标准

Swift 的错误处理工具,会根据处理失败的时机和需要保留的信息发挥不同作用。本文结合执行结果,整理 throws、do-catch、try?、try! 和 Result 的选择方式。

7 分钟阅读
[Swift 基础 #5] throws、try、Result 的选择标准 封面图

学习 Swift 错误处理时,很容易觉得工具太多:throws 和 do-catch、try 后面的问号和感叹号,以及 Result 类型。有些 API 会抛出错误,有些 API 会返回 Result,还有些代码直接用 try? 把错误吞掉。到底哪一种才是标准?

实际上,这些工具并不是相互竞争,而是各司其职。默认选择是 throws;try 的变体构成了“你有多在意错误”的光谱;Result 则是在需要把错误作为值携带时的补充工具。Swift 基础系列第 5 篇,本文将画出这张地图。

输入验证中优先关闭失败路径的结构,与前一篇的 Swift guard 与提前退出的选择标准 相关。

基础 — 错误也是类型,抛出也是契约

Swift 错误处理从两个声明开始。将错误定义为遵循 Error 协议的类型,并在可能产生错误的函数签名中添加 throws。

enum PaymentError: Error {
    case insufficientBalance(needed: Int)
    case cardExpired
    case network(underlying: Error)
}

func pay(amount: Int) throws -> Receipt {
    guard balance >= amount else {
        throw PaymentError.insufficientBalance(needed: amount - balance)
    }
    // ...
}

错误类型常用 enum 并非偶然。正如可选值一文所见,enum 是表达“各种情况”的工具,而失败原因正是各种情况。通过关联值,还可以携带金额不足等上下文信息。

更重要的是,throws 出现在签名中。类型系统会记录这个函数可能失败,因此调用方必须使用 try,漏写 try 就会产生编译错误。不同于无法知道异常会在哪里发生的语言,Swift 代码会用 try 标记所有可能失败的位置。正如可选值将“没有值”提升为类型,throws 也将“可能失败”提升到签名中。第 1 篇中的安全优先原则在这里同样适用。

接收端的基本形式是 do-catch。

do {
    let receipt = try pay(amount: 50_000)
    show(receipt)
} catch PaymentError.insufficientBalance(let needed) {
    showTopUp(needed: needed)
} catch {
    showError(error)  // 其余全部, error 自动提供变量
}

catch 使用模式匹配。它可以只捕获特定 case、取出关联值,再用最后一个 catch 接收其余情况,结构类似 switch。这种设计与错误是 enum 这一事实相互配合。

try 的三种面貌 — 对错误的关注程度光谱

try 有三种写法,每一种都声明了“要如何对待错误”。

try — 我会处理,或者继续传递。 这是默认形式。可以用 do-catch 捕获错误,或者将自己的函数也声明为 throws,让错误向上层传递。错误传播会自动完成,无需显式代码,这是 Swift 错误处理隐藏的优势之一。中间层函数只需添加 throws,就能免费承担管道作用;处理可以统一放在更靠近 UI 的外层完成一次。

try? — 失败也没关系,只要没有值即可。 它会把错误转换为可选值:成功时得到值,失败时得到 nil,同时丢弃错误信息。适合读取缓存这类“失败就算了”的场景。危险的是习惯性使用 try?。在需要失败原因的地方使用 try?,会让调试线索悄悄消失。只有当你能对“没人关心这次失败的原因吗”回答“是”时,才应该使用它。

try! — 失败就是程序员的错误。 失败时会立即崩溃。和强制解包 ! 的逻辑完全相同,它只能用于“如果失败,就说明代码错了”的地方,例如加载 App bundle 中内置的资源。可选值一文中的标准在这里原样适用:如果 nil(这里是错误)属于正常场景,就绝对禁止使用。

try 的三种变体,是对待错误的态度声明
try 的三种变体,是对待错误的态度声明

Result — 将错误作为值携带

如果 throws 是默认选择,那么什么时候使用 Result?Result 是一个承载成功或失败的 enum。

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

它与 throws 的决定性区别在于时间和地点。throws 会强制在调用时立即处理(或传播)错误,而 Result 是普通值,可以保存、放入数组,稍后再处理。因此,适合使用 Result 的场景大致有三种。

第一,基于 completion handler 的异步 API。正如闭包一文所见,completion handler 会在函数返回后执行,因此无法通过 throws 传递错误。completion: (Result<Data, NetworkError>) -> Void 曾是这一场景的标准做法。第二,需要汇总结果时。要运行 10 个任务并统计 7 个成功、3 个失败,错误就必须是值。第三,希望明确指定失败类型时。Result 的 Failure 是具体类型,因此从签名中就能看出可能出现哪种错误。

不过,也需要了解发展方向。async/await 成为标准后,异步错误传递由 async throws 负责,Result 的第一种用途正在新代码中减少。第三种用途也正被 Swift 6 的 typed throws(throws(PaymentError)、SE-0413)吸收。因此,目前的实践标准可以这样总结:默认使用 throws;Result 用于必须将错误作为值保存、汇总或传递的特殊场景。

顺带一提,两者之间的转换只需一行。用 Result { try pay(amount: 100) } 包装,再用 try result.get() 拆开。既然可以在边界自由转换,就没有坚持只用其中一种的理由。

设计感 — 好的错误会考虑接收方

下面稍微谈谈语法之外的内容。错误处理代码的质量,很大程度上取决于抛出方的设计。

将错误拆分为调用方可以采取不同处理方式的单位。 细分 case 的标准是:“接收方会以不同方式处理这两种情况吗?”余额不足和卡片过期需要不同的用户提示,因此应该是不同 case。如果 App 对 TCP 超时和 DNS 失败采用相同的重试方式,就应该将它们归为 network。把处理方式相同的错误拆成十种,只会增加 catch。

选择抛出错误还是返回可选值,标准也是一样的。 如果“不存在”是字典查询这类可预期的日常结果,就使用可选值;如果出了问题并且需要原因,就使用 throws。如果失败原因只有一个且显而易见,很多时候可选值就足够了。

将面向用户显示的消息与错误类型一起设计。 遵循 LocalizedError 后,可以让错误本身携带用于显示的消息;这一点已在另一篇文章中详细介绍。

throws 是必须立即接住的球,Result 是装在盒子里的值
throws 是必须立即接住的球,Result 是装在盒子里的值

直接执行确认的结果

我在 Apple Swift 6.3.3 中运行了一个将字符串转换为整数的 throwing 函数。用 try? 接收失败时只剩下 nil;用 Result 捕获成功结果后,稍后可以通过 get() 将其重新接入 throwing 流程。

error=try?-nil:true,result:42

正因为有这个差异,只要失败原因可能改变日志、重试或用户提示中的任何一项,我就不会使用 try?。相反,在缓存查询这类将失败和不存在同等对待的边界上,转换为 nil 更能表达意图。只有在无需立即处理结果,或需要汇总多个任务结果时,才选择 Result。

总结

  • Swift 错误处理的骨架是 Error 协议(主要是 enum)+throws 签名+do-catch 模式匹配。失败可能性会注册到类型系统,所有失败位置都会用 try 标记。
  • try 的三种变体是对待错误的态度声明:try 表示处理或传播,try? 表示不在意原因,try! 表示失败就是 bug。
  • 错误传播是自动的。中间层只需添加 throws,处理在外层完成一次。
  • Result 是在需要将错误作为值保存或汇总时使用的工具。异步传递正在由 async throws 接替,类型明确化用途则正在由 typed throws 接替。
  • 设计的核心,是将错误 case 拆分为“调用方会采取不同处理方式的单位”。

至此,Swift 基础系列的控制流三部曲(可选值、guard、错误处理)完成了。下一篇将讨论另一种基础:为什么 Swift 的 String 比其他语言特别难,并从“韩语有几个字”为什么不是一个简单问题开始。

延伸阅读

来源与验证

  • The Swift Programming Language: Error HandlingSwift.org · 官方文档 · 核查 2026年8月26日依据: Error、throw、throws、do-catch 以及 try、try?、try! 的处理方式
  • Swift ResultApple Developer Documentation · 官方文档 · 核查 2026年8月26日依据: Result 的 success、failure 表示方式与 throwing 表达式之间的转换 API