在异步网络请求的语境下,选择 Result 还是 async throws 取决于你的并发框架(是传统的 GCD 闭包,还是现代的 async/await)以及你对状态管理的需求。
在 Swift 5.5 之后,行业标准已大幅向 async throws 倾斜,但在特定防御式场景下,Result 仍有其不可替代的价值。
1. 深度对比:Result vs. async throws
Result<T, Error>:数据的持有者
Result 是一个显式的状态容器。它将“成功”或“失败”封装成一个值。
-
优势:
- 可存储性:你可以把请求结果存入
ViewModel的属性中,随时供 UI 渲染或重新尝试。 - 异步兼容性(旧版) :在闭包回调中,
Result是唯一的选择,因为它能确保回调一定且仅返回一个值。 - 延迟处理:你可以在网络层拿到
Result,但不立即拆解,而是传递到业务层再处理。
- 可存储性:你可以把请求结果存入
-
劣势:
- 语法冗长:需要大量的
switch或flatMap。 - 容易遗漏:如果你在闭包里忘记调用
completion(result),编译器不会报错,导致请求“石沉大海”。
- 语法冗长:需要大量的
async throws:控制流的驱动者
这是现代 Swift 的原生方式。它把异步错误处理变得像同步代码一样直观。
-
优势:
- 代码极简:消除嵌套回调,支持线性阅读。
- 强制性:编译器强制你写
try,不处理错误就无法编译通过,消除了“忘记回调”的风险。 - 自动传播:错误可以跨越多个异步层级自动冒泡,无需手动转发。
-
劣势:
- 瞬时性:错误发生后如果不立即捕获,它就“消失”在调用栈中了,无法像
Result那样被当做变量传来传去。
- 瞬时性:错误发生后如果不立即捕获,它就“消失”在调用栈中了,无法像
2. 场景化选择建议
场景 A:复杂的并发任务组 (Task Group)
推荐方案:async throws
当你需要同时发起 5 个请求并等待全部完成时,async throws 配合 TaskGroup 或 async let 可以让你用几行代码处理并发和错误。如果其中一个请求失败,整个链路会自动短路,非常高效。
场景 B:UI 状态绑定(如 SwiftUI)
推荐方案:Result
如果你需要记录“最后一次请求的状态”以显示不同的 UI(加载中、成功、错误页面),在 ViewModel 中定义一个 @Published var requestStatus: Result<User, Error>? 会非常方便。
场景 C:防御后端“不规范” API
推荐方案:async throws 内部封装解析逻辑
在网络层使用 throws 拦截所有的 HTTP 错误(如 401, 500)和 JSON 解析错误。只有干净、合法的数据才会被 return 出来。
3. 两者的“桥梁”:.get() 与 init(catching:)
在防御式编程中,你经常需要在这两种模式间切换。Swift 提供了内置转换工具:
-
从
Result转为throws:当你拿到一个
Result但想用同步风格处理它时:Swift
let data = try result.get() // 成功则解包,失败则直接抛出 -
从
throws转为Result:当你调用一个异步抛出函数,但想把结果存起来时:
Swift
let result = await Result { try await networkService.fetch() }
4. 总结:性能与安全的平衡
| 维度 | Result | async throws |
|---|---|---|
| 错误发现 | 运行至该行时处理 | 编译阶段强制要求处理 |
| 内存开销 | 略高(涉及枚举包装) | 极低(基于寄存器跳转) |
| 可读性 | 较低(嵌套多) | 极高(平铺直叙) |
| 适用环境 | GCD 闭包、状态存储 | Swift Concurrency、业务逻辑链 |
防御式金律:在网络层和解析层使用 async throws 以确保错误不被遗漏且逻辑清晰;在**表示层(ViewModel/UI)**如果需要持久化错误状态,再将其转换为 Result。