Rx.NET中是否存在仅在值变化时发射数据的BehaviorSubject类Subject实现?自制DistinctSubject与Observable.DistinctUntilChanged组合方案对比疑问
嘿,这个问题问得相当到位——我之前在做WPF项目用Rx替代INotifyPropertyChanged的时候,也纠结过要不要自己封装类似的Subject,后来踩了几个坑才明白原生操作符的好处。咱们来掰扯掰扯你这个实现相比用DistinctUntilChanged组合的方案,存在哪些劣势:
1. 灵活性严重不足
你的DistinctSubject<T>硬编码了用EqualityComparer<T>.Default做相等判断,虽然可以通过加构造函数参数支持自定义比较器,但原生的DistinctUntilChanged支持在调用时动态传入任意比较逻辑,还能和其他操作符无缝组合。比如:
- 如果你想先把值映射成另一种类型再去重:
source.Select(x => x.Name).DistinctUntilChanged() - 如果你想忽略大小写的字符串去重:
source.DistinctUntilChanged(StringComparer.OrdinalIgnoreCase)
这些场景下,你的DistinctSubject要么得修改类代码,要么得额外封装,完全没有操作符组合的灵活度。
2. 违背Rx的核心设计原则
Rx的精髓是操作符组合+单一职责:Subject的定位是作为"桥梁"(比如把命令式的推值转换成响应式流),而业务逻辑(比如去重、过滤)应该交给专门的操作符来处理。你把去重逻辑塞进Subject里,相当于把两个职责混在一起,会导致:
- 代码复用性差:如果另一个场景需要不同的去重逻辑,你得再写一个
DistinctSubject2<T>,而用操作符的话直接复用DistinctUntilChanged就行 - 可读性下降:其他开发者看到你的
DistinctSubject,得点进源码才知道它做了去重,而用操作符链的话,光看方法名就明白整个流的处理逻辑
3. 状态管理的隐藏风险
你的实现依赖_subject.Value来判断当前值,但BehaviorSubject.Value有几个坑:
- 如果Subject已经调用了
OnCompleted或OnError,访问Value会直接抛出异常,你的OnNext方法里没有处理这种情况,会导致崩溃 - 当多线程并发推值时,
_subject.Value的读取和判断可能存在线程安全问题(虽然BehaviorSubject本身是线程安全的,但你的判断逻辑是if (!Equals(value, _subject.Value)),这是一个非原子操作,极端情况下可能出现竞态条件)
而DistinctUntilChanged是基于流的状态来维护前一个值,它的内部逻辑是线程安全的,也不会直接访问Subject的Value属性,避免了这些风险。
4. 调试和可观测性差
用操作符组合的流,你可以轻松插入调试操作符,比如:
source .Do(x => Console.WriteLine($"原始值: {x}")) .DistinctUntilChanged() .Do(x => Console.WriteLine($"去重后的值: {x}")) .Subscribe();
但你的DistinctSubject把去重逻辑封装在内部,想要调试的话只能修改类的源码,比如在OnNext里加日志,非常不方便。
5. 缺乏原生操作符的成熟性
Rx的官方操作符都是经过大量测试的,覆盖了各种边界场景:比如null值处理、相等比较器抛出异常的情况、空流的处理等等。而你自己写的DistinctSubject可能没考虑到这些细节,比如当T是值类型且为默认值时的判断,或者自定义Equals方法抛出异常时的处理,这些都可能导致潜在的bug。
替代方案
其实你完全不需要自己封装Subject,用原生API就能实现需求:
// 先创建BehaviorSubject作为数据源 var behaviorSubject = new BehaviorSubject<T>(initialValue); // 对它应用DistinctUntilChanged得到去重后的流 var distinctObservable = behaviorSubject.AsObservable().DistinctUntilChanged(); // 推值的时候用behaviorSubject.OnNext(value) // 订阅的时候用distinctObservable.Subscribe(observer)
如果需要像你的DistinctSubject那样提供Value属性,可以再封装一层简单的包装类,既保留操作符的灵活性,又满足你的使用习惯。
内容的提问来源于stack exchange,提问作者noseratio

