在 Combine 中,.sink 和 .assign 是最常用的两个终端操作符(Subscribers) 。它们的主要区别在于赋值意图、生命周期管理以及对错误的处理方式。
1. .sink:多功能的“瑞士军刀”
.sink(接收)是最通用的订阅方式。它允许你通过闭包手动处理流中的每一个信号。
-
核心特质:完全控制。你可以处理数据,也可以处理完成/错误事件。
-
适用场景:
- 执行非赋值类的操作(如打印日志、持久化存储、触发另一个网络请求)。
- 需要处理错误逻辑(
.failure)。 - 需要对数据进行条件判断后再操作。
Swift
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished: print("任务完成")
case .failure(let error): print("发生错误: (error)")
}
},
receiveValue: { value in
print("收到值: (value)")
}
)
2. .assign:声明式的“传输带”
.assign(赋值)是一个专门为了属性绑定优化的快捷方式。它直接将 Publisher 发出的值设置到某个对象的属性上。
-
核心特质:简洁与强约束。它要求 Publisher 的错误类型必须是
Never(即该流永远不会报错)。 -
两个变体:
.assign(to:on:):传统的赋值方式。.assign(to: &$variable):专门为 SwiftUI@Published属性设计的安全方式。
.assign(to:on:) 的坑:
它会强引用 on 参数中的对象。如果这个对象(如 self)同时也持有了该订阅集合(cancellables),就会导致内存泄漏(Retain Cycle) 。
.assign(to: &$variable) 的优势:
这是 Swift 5.3+ 推出的“黑科技”。它不会产生循环引用,因为它会自动管理内部生命周期,且不返回 AnyCancellable,因为它直接将生命周期绑定到了目标 @Published 属性上。
3. 核心区别对比
| 特性 | .sink | .assign |
|---|---|---|
| 错误处理 | 必须显式处理(除非 Failure 是 Never) | 必须先将 Failure 转为 Never 才能使用 |
| 赋值逻辑 | 闭包内手动赋值 | 自动完成赋值 |
| 内存风险 | 需要配合 [weak self] 规避 | to:on: 极易导致内存泄漏 |
| 返回值 | 返回 AnyCancellable | to:on: 返回 AnyCancellable;to:&$ 不返回 |
| 代码风格 | 过程式、灵活 | 声明式、简洁 |
4. 决策矩阵:我该选哪个?
-
如果你要把数据存进
@Published属性:- 优先选
.assign(to: &$data)。这是最安全、代码量最少的做法。
- 优先选
-
如果你要更新一个 UI 控件(如 UILabel.text) :
- 先用
.replaceError(with: "")确保不会报错,然后用.assign(to: .text, on: label)。
- 先用
-
如果你需要执行一段逻辑(如弹窗、跳转、打印) :
- 必须使用
.sink。
- 必须使用
-
如果你不确定这个流会不会报错:
- 使用
.sink并在receiveCompletion中统一处理错误。
- 使用
5. 防御式建议:关于 assign 的安全性
如果你不得不使用 .assign(to:on:) 且担心循环引用,又不想写冗长的 sink + [weak self],可以考虑在链条末尾先转换:
Swift
publisher
.replaceError(with: "默认值")
.sink { [weak self] in self?.label.text = $0 } // 比直接用 assign(to:on:) 更安全