委托和事件的核心是引用关系与生命周期管理,误用导致内存泄漏、事件失效或崩溃;需明确谁持有谁、谁负责清理,并在订阅后及时取消。

委托和事件不是“怎么用”的问题,而是“谁持有谁”“谁该负责清理”的问题。用错的核心表现是内存泄漏、事件不触发、多线程调用崩溃——这些都不是语法错误,而是对引用关系和生命周期的误判。
Delegate.Combine 和 += 的行为差异
用 += 订阅事件时,编译器会自动调用 Delegate.Combine;但手动调用 Delegate.Combine 会产生新委托实例,而 += 是在原字段上做合并(底层仍调用 Combine,但封装了字段访问逻辑)。
-
Delegate.Combine返回新对象,原委托不变;频繁调用会创建大量临时数组(因为多播委托内部用object[]存调用列表) -
+=看似简洁,但若目标字段是null,Combine(null, x)返回x,行为安全;但若字段被多线程并发修改,可能丢失订阅(无锁) - 自定义事件的
add/remove块里必须用Delegate.Combine和Delegate.Remove,不能直接赋值,否则破坏多播链
事件触发时的 null 检查为什么用 ?.Invoke() 而不是 if (e != null) e.Invoke()
看起来等价,但后者有竞态条件:检查通过后,另一个线程可能已把事件置为 null,导致 NullReferenceException。
-
e?.Invoke()是原子性操作(JIT 保证),.NET Core 2.1+ 及所有现代运行时都支持 - 旧项目若需兼容 .NET Framework 4.0,可用
Interlocked.CompareExchange(ref e, null, null)先快照再调用,但更重 - 不要写
e?.Invoke(args)后再单独处理null分支——事件就是可为空的契约,没订阅就是什么也不做
观察者模式中,为什么推荐用事件而不是公开委托字段
公开委托字段(如 public Action OnDataReceived;)等于放弃封装:外部能直接赋值 =,覆盖全部监听器,且无法拦截订阅逻辑。
- 事件强制使用
+=/-=,天然支持多播,也天然阻止外部替换整个委托链 - 自定义事件的
add块可加入日志、限流、线程切换(如 WPF 的Dispatcher.Invoke) - 关键点:事件不是“语法糖”,它是编译器生成的线程安全访问器——
add内部用Delegate.Combine,remove用Delegate.Remove,二者都处理了空值与重复移除 - 但注意:事件访问器本身不加锁,高并发场景仍需手动同步(比如在
add中加lock)
委托引用实例方法时的内存泄漏风险
这是观察者模式中最隐蔽的坑:一个长生命周期对象(如窗体、服务类)订阅了短生命周期对象(如某次请求的 ViewModel)的事件,后者就永远无法被 GC 回收。
- 根本原因:
EventHandler或Action委托持有了目标实例的强引用(存于_target字段) - 典型场景:在 WPF UserControl 构造函数中订阅
Application.Current.Exit,却没在Unloaded中取消——UserControl 卸载后仍被 Application 引用 - 解法不是“不用事件”,而是明确生命周期边界:谁创建订阅,谁负责取消;或改用弱事件模式(
WeakEventManager)、WeakReference<T>封装回调 - 调试技巧:用 Visual Studio 的“内存使用情况”快照对比,筛选未释放的 ViewModel 类型,再看其根引用链是否经过某个委托字段
委托和事件的复杂点不在语法,而在它们悄悄建立的引用链——这条链看不见、摸不着,却决定对象能不能被回收、事件会不会静默失效。写完 +=,一定要问一句:我什么时候 -=?


















