在 Swift 中,Result<T, Error> 和 throws 都可以处理错误,但它们在思维模型和代码流向上完全不同。
简单来说:Result 是“数据”,throws 是“行为”。
以下是它们在调用方和链式操作上的核心区别:
1. 调用方的语法体验:显式 vs 隐性
Result (显式处理)
Result 是一个枚举。当你调用一个返回 Result 的函数时,编译器会得到一个值。
- 处理方式:你必须通过
switch或if case手动拆解。 - 特点:强制你在当前位置面对错误,逻辑无法“自动冒泡”。
Swift
let result = fetchData()
switch result {
case .success(let data):
print("拿到数据: (data)")
case .failure(let error):
print("处理错误: (error)")
}
Throws (隐性处理/冒泡)
throws 函数在调用时必须加上 try。
- 处理方式:它允许你不做处理(继续向上层抛出),或者用
do-catch统一捕获。 - 特点:支持“短路机制”,即一行报错,后续代码自动跳过。
Swift
do {
let data = try fetchData()
print("拿到数据: (data)")
} catch {
print("处理错误: (error)")
}
2. 链式操作的区别:函数式 vs 命令式
这是两者差异最明显的地方。
Result 的链式:转换流 (Transformation)
Result 依赖其内置的函数式方法,如 map 和 flatMap。
- 逻辑:你是在对一个“容器”进行操作。如果前一步失败,后面的
map会被自动忽略。 - 优势:非常适合在不同函数间传递结果(例如作为属性存储或在闭包间传递)。
Swift
let finalResult = fetchData()
.flatMap { data in parse(data) } // 如果 fetchData 失败,跳过 parse
.map { model in model.id } // 提取 ID
// 此时 finalResult 依然是一个 Result 对象
Throws 的链式:线性流 (Linear Flow)
throws 的链式操作依赖语法层的“顺序执行”。
- 逻辑:代码看起来就像“一切顺利”的纯线性过程。
- 优势:代码可读性极高,符合人类阅读习惯。
- 隐患:如果中间某一步崩溃或逻辑断层,很难一眼看出错误是在哪一层产生的。
Swift
func getID() throws -> Int {
let data = try fetchData() // 失败则直接退出函数
let model = try parse(data) // 失败则直接退出函数
return model.id
}
3. 关键特性对比表
| 特性 | Result<T, Error> | Throws |
|---|---|---|
| 本质 | 这是一个状态值(枚举) | 这是一个控制流(类似跳转) |
| 存储性 | 可存储。可以存入数组或属性中 | 不可存储。必须立即执行或冒泡 |
| 嵌套 | 容易出现嵌套,除非使用 flatMap | 结构扁平,代码非常整洁 |
| 错误类型 | 可以通过泛型 Result<T, MyError> 强类型化 | 默认是 any Error(Swift 6 开始支持 Typed Throws) |
| 性能 (成功时) | 略慢(需要包装成枚举) | 极快(几乎没有额外开销) |
| 性能 (失败时) | 恒定(枚举分支切换) | 略慢(涉及错误包装和跳转) |
4. 架构建议:如何平衡?
在防御式编程中,我们通常遵循以下准则:
- 内部逻辑转换用
throws:在模块内部、或是简单的业务流(A -> B -> C)中,使用throws可以让代码最简洁。 - 跨异步边界用
Result:在非 Swift Concurrency 的异步回调中,必须用Result传递状态。 - 状态保存用
Result:如果你需要记录某个操作的最后一次执行结果(例如lastUploadResult),应使用Result属性。
总结一句话:
如果你想写出像诗一样简洁的逻辑链路,选 throws;如果你需要精确控制数据在不同时间和空间(线程)的流转,选 Result。