在 Swift 现代并发(Swift Concurrency)体系下,async/await 已经成为了处理异步流的主流方式。然而,Result 并没有退场,它从“流程控制工具”转型为了**“状态保存工具”**。
在架构设计中配合使用这两者时,需要注意以下几个核心点,以保持代码的健壮性:
1. 明确分工:throws 负责流转,Result 负责存储
这是最基本的设计原则。
- 执行层(Executing) :函数应当声明为
async throws。这使得你可以利用await语法的简洁性,并让错误通过 Swift 运行时的异常机制自动传播。 - 持有层(Holding) :当异步操作完成,你需要将结果存入
ViewModel或State供 UI 消费时,将其包装为Result。
2. 避免在 Task 闭包中丢失错误
当你从同步环境进入异步环境时,Task 会隐式地捕获错误。如果你不小心,错误可能会被“吞掉”。
Swift
// ⚠️ 危险做法:错误在 Task 内部发生,外部无法得知
Task {
let user = try await service.fetchUser()
self.user = user
}
// ✅ 防御式做法:将结果捕获为 Result 并显式处理
Task {
let result = await Result { try await service.fetchUser() }
await MainActor.run {
self.loadingResult = result // 将 Result 存入状态,驱动 UI
}
}
3. 利用 Result 处理“非阻塞”的并发任务
如果你发起多个独立的异步请求,且不希望其中一个失败就导致整个流程崩塌(async let 或 TaskGroup 默认的短路行为),可以先将每个任务的结果包装为 Result。
Swift
async let userResult = Result { try await fetchUser() }
async let postsResult = Result { try await fetchPosts() }
// 这里不会因为任何一个请求失败而抛出,你可以分别处理成功和失败的部分
let user = try? (await userResult).get()
let posts = (try? (await postsResult).get()) ?? []
4. 类型化错误(Typed Throws)的衔接
Swift 6 引入了 Typed Throws。在设计 Result 与 async 配合的 API 时,利用明确的错误类型可以极大增强防御性。
Swift
// 这种定义让编译器保证了:要么得到 User,要么得到特定的 APIError
func loadUser() async -> Result<User, APIError> {
do {
let user = try await fetchRemoteUser()
return .success(user)
} catch let error as APIError {
return .failure(error)
} catch {
return .failure(.unknown)
}
}
5. 状态机设计:从 Loading 到 Result
在结合 SwiftUI 或异步 UI 时,Result 是定义状态机的完美工具。配合 async/await,你可以消除复杂的布尔逻辑(如 isLoading, hasError)。
| UI 状态 | 实现方式 |
|---|---|
| 初始/加载中 | nil (Optional Result) |
| 执行成功 | .success(data) |
| 执行失败 | .failure(error) |
总结:防御式组合建议
- 内部 API:优先使用
async throws,保持代码扁平化,减少嵌套。 - 跨组件/状态存储:使用
Result。它提供了一个稳定的、可传递的快照,反映了异步任务在某一时刻的确切结局。 - 桥接技巧:熟练使用
Result(catching:)初始化器将异步抛出转换为值,以及使用result.get()将保存的值重新投入抛出链条中。