最稳妥的观察者实现是event+EventHandler<T>;ISubject/IObserver仅适用于Rx.NET流式场景,其完成语义与离散事件不匹配,且易致内存泄漏和调试困难。

直接用 event + EventHandler<T> 是最稳妥、最符合 .NET 习惯的观察者实现方式;手写 ISubject/IObserver 接口只在教学或极端定制需求下才值得考虑。
为什么别自己写 ISubject/IObserver?
不是不能写,而是容易陷入“为模式而模式”的陷阱。标准接口 ISubject 和 IObserver<T> 在 .NET 中本就存在,但它们是为 System.Reactive(即 Rx.NET)流式场景设计的,和日常事件通知不匹配。
-
IObserver<T>.OnNext()要求你主动调用,且必须配对OnCompleted()或OnError()—— 但按钮点击、配置变更这类离散事件根本不存在“完成”语义 -
ISubject实现需手动管理订阅生命周期,Subscribe()返回的IDisposable若未显式Dispose(),极易引发内存泄漏(比如监听了窗体Closed事件却忘了取消) - 调试困难:委托链被封装在
IObservable内部,异常堆栈不直观;而event的调用点清晰可见
event 声明时必须避开的三个坑
event 看似简单,但错用会破坏封装性或导致运行时崩溃。
- 别暴露
public Action<T> MyAction:外部可直接赋值MyAction = null或替换整个委托链,彻底绕过发布逻辑 - 必须用
EventHandler<T>,而非裸Action<T>:前者带object sender参数,能区分多个发布源(比如两个DataPublisher实例共用一个处理方法) - 事件参数必须继承
EventArgs:传string或object会导致后期扩展困难(比如想加时间戳、来源 ID),且 IDE 不提供智能提示
正确写法示例:
public class DataPublisher
{
public event EventHandler<DataChangedEventArgs> DataChanged;
public void Notify(string value)
{
DataChanged?.Invoke(this, new DataChangedEventArgs(value));
}
}
public class DataChangedEventArgs : EventArgs
{
public string Value { get; }
public DateTime Timestamp { get; } = DateTime.Now;
public DataChangedEventArgs(string value) => Value = value;
}
Observable.FromEventPattern 是桥接 event 的唯一安全方式
当你确实需要把已有 event 转成 IObservable<T>(比如做节流、去抖、合并多个事件流),必须用 Observable.FromEventPattern,而不是手写 Observable.Create。
-
FromEventPattern自动处理线程上下文切换(比如从 UI 线程触发的Click事件,能在指定调度器上执行后续操作) - 它内部做了异常隔离:某个观察者的
OnNext抛异常,不会中断其他观察者 - 返回的
IDisposable真正释放底层事件监听,而手写Create很难保证-=行为与+=完全匹配(Lambda 表达式每次都是新实例)
错误示范(手写桥接):
// ❌ 危险:lambda 每次都是新委托,-= 无法移除
var obs = Observable.Create<int>(o =>
{
button.Click += (s, e) => o.OnNext(1);
return () => button.Click -= (s, e) => { }; // 这里移除的是另一个委托!
});
正确写法:
// ✅ 安全:FromEventPattern 确保 += / -= 成对
var clickStream = Observable.FromEventPattern<EventArgs>(
h => button.Click += h,
h => button.Click -= h
).Select(_ => 1);
多线程环境下 event 的调用要加空检查但不必锁
event 字段本身是线程安全的(+= 和 -= 是原子操作),但调用前的空检查 DataChanged?.Invoke(...) 不是原子的 —— 可能刚判空完,另一线程就把所有订阅者清空了,导致 NullReferenceException。
- 解决方法就是用局部变量缓存委托引用:
var handler = DataChanged; if (handler != null) handler(this, args); - 不需要用
lock包裹整个调用:委托链是不可变的,缓存后调用是安全的 - 如果观察者内部有共享状态(比如更新 UI 控件),那是观察者自己的责任,应在观察者内处理跨线程访问(如用
Control.Invoke)
真正容易被忽略的是:事件通知的语义是否适合并发。比如一个 ProgressChanged 事件,若频繁触发且顺序敏感,应考虑加锁或改用线程安全队列批量分发 —— 但这是业务逻辑层的事,不是 event 机制本身的问题。


















