无法在sink块中更新同一@Published变量,此现象是否符合预期?
问题解答
这是预期行为,和竞态条件无关——本质是Combine框架为了避免无限递归或嵌套触发,给@Published的发送逻辑加了保护限制,和你提到的willSet里设置值不会触发willSet是类似的设计思路。
背后的逻辑
当你在@Published变量的sink回调里直接修改该变量时,Combine会抑制这次修改触发的新订阅通知,甚至在当前回调的执行流程中,这个赋值的效果会被框架内部逻辑覆盖。这么做是为了防止出现无限循环(比如在sink里反复修改变量导致回调被无休止调用)。
你的代码具体分析
看你的示例代码:
- 调用
presenter.state = .loading后,@Published的didSet触发,把.loading发送给sink回调。 - 进入
sink后你尝试设置state = .finish,但此时正处于@Published处理上一次值变更的流程中,Combine会阻止这次赋值生效,也不会触发新的sink调用。 - 所以最后
print(presenter.state)输出的还是.loading,完全符合这个机制的表现。
另外提一句:原代码里在sink里判断self.state == .ready是逻辑错误——因为sink触发时,state已经更新为newValue(也就是.loading)了,这个分支原本不应该被命中,可能是你测试时的笔误或者对@Published的触发时机理解有误。
解决方法
如果需要在状态变更后触发后续的状态更新,把赋值逻辑异步调度到当前事件循环之后执行即可,比如用Task包裹:
$state.sink { [weak self] newValue in guard let self else { return } switch newValue { case .loading: print("making this finish") // 异步调度,绕开当前sink的执行流程限制 Task { @MainActor in self.state = .finish } default: break } } .store(in: &cancellables)
这样state = .finish会在当前sink回调执行完之后再执行,就能正常更新状态并触发后续的sink通知了。
内容的提问来源于stack exchange,提问作者Wy th
相关产品推荐
相关产品推荐

