多播委托赋值必须用+=而非=,否则覆盖原有方法;调用前需用?.判空;移除时方法引用须完全一致;异常会中断后续调用,需手动遍历+try/catch处理。

多播委托赋值时别用 =,要用 +=
直接拿 = 赋值,等于把之前的委托全都扔了,这不是追加,这是替换。C# 的多播委托底层其实是个链表,只有 += 才能把多个方法“缝合”到同一个委托实例上。
一个常见翻车现场:
myDelegate = MethodA; myDelegate = MethodB; myDelegate(); // 只调了 MethodB,MethodA 没了
正确的姿势是:
Action handler = null; handler += MethodA; handler += MethodB; handler(); // 依次调用 MethodA → MethodB
+=背后其实是Delegate.Combine,自动帮你处理空值和类型检查- 就算左边是
null,+=也能安全初始化,不用提前判空 - 别手动去调
Delegate.Combine(a, b),容易出错,可读性也差
调用多播委托前必须判空,否则 NullReferenceException
就算你刚用 += 加过方法,也别拍胸脯说 handler 一定非空——万一所有订阅者都用 -= 撤了,或者初始就是 null,那直接调用就炸了。
错误示范:handler(); —— 运行时直接给你一个 NullReferenceException。
安全写法(推荐):
handler?.Invoke();
?.是 C# 6.0+ 引入的空条件操作符,写法简洁,而且比if (handler != null) handler();更靠谱(线程安全考虑)- 别在
handler?.Invoke()外面套一层try/catch——异常是设计缺陷,不是流程分支 - 如果委托有返回值(比如
Func),?.会返回null或默认值,需要根据场景单独处理
移除委托必须用 -=,且方法引用要完全一致
-= 不是模糊匹配,它严格比较方法指针,连闭包捕获的变量都得一模一样。稍微差一点,移除就失败,结果就是内存泄漏或者方法重复执行。
典型陷阱:
handler += () => Console.WriteLine("A");
handler -= () => Console.WriteLine("A"); // ❌ 失败!这是另一个匿名方法实例
- 命名方法可以安全移除:
handler += MethodA; handler -= MethodA; - Lambda 要复用的话,先定义一次再引用:
var lambda = () => ...; handler += lambda; handler -= lambda; - 用
Action或Func<>时,不要在循环里反复创建新委托实例然后又移除,那是给自己挖坑
多播委托异常会中断后续调用,无法自动跳过
这是最容易被忽略的设计事实:一旦某个订阅方法抛了异常,整个链就停了,后面的方法永远没机会执行。
比如:
handler += () => { Console.Write("1"); };
handler += () => { throw new Exception(); };
handler += () => { Console.Write("3"); }; // ❌ 永远不会执行
- 框架没有内置的“容错遍历”机制,唯一的可控方式是
GetInvocationList()+ 手动 try/catch - 如果业务要求健壮性,必须显式遍历:
foreach (var d in handler.GetInvocationList()) { try { d.DynamicInvoke(); } catch { /* 忽略或记录 */ } } - 性能上要注意:反射调用
DynamicInvoke比直接Invoke慢一个数量级,高频场景要慎用
多播委托不是简单的“语法糖”,它的调用顺序、异常传播、生命周期管理全由底层 Delegate 类控制。写的时候少想“链式”,多想“事件队列”——每个环节都得自己兜底。