优先使用 .NET 原生 IObservable + Observer.Create(),它线程安全、支持取消与错误传播;避免手写 Subject 或滥用 event,正确处理空事件调用、Dispose 和调度上下文。

Observer 和 IObservable 是 .NET 原生支持,不是“自己造轮子”的首选
如果你在写新项目或重构旧逻辑,优先用 IObservable<t></t> + Observer.Create(),而不是手写 Subject 类或事件委托链。.NET Framework 4.0+ 和所有现代 .NET(5/6/7/8)都内置了响应式扩展(System.Reactive),它比手动维护订阅列表更健壮、线程安全、支持取消和错误传播。
常见错误是把 event 当成观察者模式等价物——事件没有取消订阅语法糖、不处理异常传递、无法链式组合(比如 .Where().Select().Take(5))。而 IObservable<t></t> 天然支持这些。
- 用
Observable.FromEventPattern()包装 UI 事件(如按钮点击),避免手动+=/-= -
Observer.Create<string>(onNext: Console.WriteLine, onError: ex => Log.Error(ex), onCompleted: () => Console.WriteLine("done"))</string>可一次性指定三类回调 - 订阅后务必保留
IDisposable返回值,调用.Dispose()才算真正取消——漏掉这步会导致内存泄漏
用 event 实现简易观察者时,必须防御 null 订阅者
event 本身不是线程安全的,且触发前不检查是否为空会直接抛 NullReferenceException。这不是风格问题,是运行时必踩的坑。
正确写法永远是:
public event Action<string> OnMessage;
// 触发时:
OnMessage?.Invoke("data");
别写 if (OnMessage != null) OnMessage("data");——中间可能被其他线程取消订阅,导致空引用。
- 如果需要线程安全分发,用
lock包住整个调用(但会降低吞吐),或改用ConcurrentQueue<action></action>缓存通知再批量执行 - 不要在事件回调里长时间阻塞(如同步 HTTP 调用),否则拖慢所有订阅者;考虑用
Task.Run(() => { ... })卸载到后台线程 - 事件参数建议封装为不可变类(
record或readonly struct),避免订阅者意外修改影响其他监听方
自定义 Subject 时,Dispose() 必须清理所有订阅并阻止后续 Subscribe()
手写 Subject<t></t> 类(比如继承 IObservable<t></t> 和 IObserver<t></t>)时,最常忽略的是:订阅者调用 .Dispose() 后,你还允许新订阅进来,或者没清空内部列表。
典型错误结构:
private List<IObserver<T>> _observers = new();
public IDisposable Subscribe(IObserver<T> observer) {
_observers.Add(observer); // 没检查是否已 disposed!
return new Unsubscriber(_observers, observer);
}
正确做法是加一个 bool _isDisposed 标志,并在 Subscribe() 开头判断:
if (_isDisposed) throw new ObjectDisposedException(nameof(MySubject));
-
Unsubscriber的Dispose()方法里,除了从列表移除,还要设_isDisposed = true(如果这是单次使用的 subject) - 如果 subject 需要重用(比如广播最新值给新订阅者),就得实现“热”语义,此时要用
ReplaySubject<t></t>或BehaviorSubject<t></t>——别自己重复造 - 手动管理生命周期时,
Subscribe()返回的IDisposable对象不能是匿名委托闭包,否则 GC 无法及时回收;应显式定义类型
调试观察者链断裂时,先看 Dispose() 调用时机和线程上下文
现象:某段逻辑明明调用了 OnNext(),但订阅者的回调就是不执行。90% 情况不是代码写错,而是 Dispose() 被提前触发,或回调发生在非预期线程(比如 UI 订阅者收到后台线程通知)。
排查步骤:
- 在
Subscribe()和Unsubscriber.Dispose()里加日志,确认谁、何时取消了订阅 - 用
Observable.SubscribeOn(Scheduler.ThreadPool)或.ObserveOn(SynchronizationContext.Current)显式控制调度,尤其 WinForms/WPF 中更新控件必须回到 UI 线程 - 如果用了
async回调(如Observer.Create(async x => await Process(x))),注意IObserver<t></t>不支持异步方法签名——必须用Task.Run包一层,否则 await 会丢失上下文
真正麻烦的是跨域(AppDomain)或跨进程场景,这时原生 IObservable<t></t> 失效,得换 gRPC 或消息队列——但那已经不属于观察者模式范畴了。


















