委托和事件是运行时真实对象,非语法糖;委托本质是方法签名的类型化容器,需严格匹配参数、返回值;event仅支持+=/-=,触发须用?.invoke()并注意线程安全与内存泄漏。

委托和事件不是语法糖,是运行时真实存在的对象;不理解委托的多播性和空安全调用,event 很容易在订阅者为 null 或跨线程时崩溃。
delegate 声明和实例化必须匹配签名
委托本质是“方法签名的类型化容器”,声明时就锁死了参数个数、类型、返回值。比如 public delegate void StatusChangedHandler(string status, bool isSuccess);,后续只能绑定签名完全一致的方法。
常见错误现象:
- 编译报错
Error CS0123 No overload for 'OnLog' matches delegate 'StatusChangedHandler'—— 方法少了一个bool参数 - 把
void OnLog(string s)绑给Func<string int></string>—— 返回值不匹配,连编译都过不去
实操建议:
- 优先用泛型内置委托:
Action<t></t>、Action<t1></t1>、Func<tresult></tresult>,避免手写重复委托类型 - 实例化时直接用方法组:
StatusChangedHandler handler = LogToConsole;,不用new StatusChangedHandler(LogToConsole) - 支持 lambda:
handler = (s, ok) => Console.WriteLine($"{s} → {(ok ? "OK" : "FAIL")}");
event 不是字段,不能直接赋值或调用
event 是对委托字段的封装,只暴露 += 和 -= 两个操作符。它背后仍是 MulticastDelegate 实例,但外部无法绕过访问器直接读写底层委托字段。
常见错误现象:
-
myObj.StatusChanged = handler;→ 编译错误:无法对事件赋值,除非在定义它的类内部 -
myObj.StatusChanged("done", true);→ 编译错误:事件只能出现在 += 或 -= 左侧 - 忘记判空直接调用:
StatusChanged?.Invoke(...)漏掉?,一有订阅者为null就抛NullReferenceException
实操建议:
- 触发事件必须走受保护的虚方法(如
OnStatusChanged),且始终用?.Invoke() - 不要在事件声明后直接初始化:
public event Action<string> Log = s => { }; </string>—— 这会让所有实例共享同一个匿名委托,且无法取消订阅 - 如果需要在线程安全场景下触发(如 UI 更新),先缓存委托再判空:
var handler = StatusChanged; if (handler != null) handler("msg", true);
订阅/取消订阅必须成对,否则内存泄漏
事件持有对订阅方法的引用,而该方法又可能持有对所在对象的引用(尤其是实例方法)。若只订阅不取消,发布者生命周期长于订阅者时,订阅者无法被 GC 回收。
使用场景:
- WinForms/WPF 控件事件(
button.Click += OnClick;)—— 必须在Dispose()或Unloaded中-= - 后台服务注册状态回调(
monitor.StateChanged += OnStateChanged;)—— 服务停用时需显式取消
实操建议:
- 用命名方法而非 lambda 订阅,否则无法取消:
monitor.StateChanged += OnStateChanged;✅,monitor.StateChanged += (s, e) => { ... };❌(无法-=) - 在
IDisposable实现中统一清理:StateChanged -= OnStateChanged; - 考虑用弱事件模式(
WeakEventManager)处理 UI 场景,但仅限 WPF
跨线程触发事件要小心委托目标线程
事件本身不关心线程,但订阅者方法执行在线程上。比如从后台线程触发 ProgressChanged,而订阅者是 WinForm 的 Label.Text =,就会抛 InvalidOperationException:“线程间操作无效”。
性能 / 兼容性影响:
- 同步触发(默认):快,但要求订阅者方法能适应当前线程上下文
- 异步派发(如
BeginInvoke、Dispatcher.Invoke):增加调度开销,但保证 UI 安全
实操建议:
- UI 类库应在文档里明确事件回调线程契约(例如 “所有事件都在 UI 线程触发”)
- 后台类不主动切换线程;把线程责任交给订阅者:
control.BeginInvoke((MethodInvoker)(() => label.Text = msg)); - 避免在事件处理中做耗时操作,否则会阻塞整个委托链
委托链的多播特性是双刃剑:它让一对多通知变得简单,但也意味着任意一个订阅者抛异常都会中断后续调用;GetInvocationList() 可以拆解链手动捕获每个异常,但多数场景下,宁可让单个失败不影响整体,也别让异常静默吞掉。










