c#事件是基于委托的发布-订阅契约,非语法糖;eventhandler强制sender+e签名,保障源追溯、null安全及设计器兼容;event关键字禁用外部赋值,仅允+=/-=,防止委托链被覆盖;触发须用?.invoke防竞态,订阅后必须显式-=防内存泄漏。

事件不是语法糖,也不是可有可无的“通知装饰”,它是 C# 中实现松耦合通信的底层契约。用错或漏掉关键环节,轻则 UI 不刷新、回调不触发,重则内存泄漏、跨线程异常崩溃。
为什么 EventHandler 比裸 delegate 更安全
直接用 public delegate void DataChangedHandler(string key, object value); 看似自由,但绕过了 .NET 的事件标准约束。而 EventHandler<t></t> 强制要求两个参数:object sender 和 T e,这带来三个实际好处:
-
sender总能准确回溯到触发源对象,避免在多个相似控件共用同一处理函数时搞混身份 - 派生自
EventArgs的泛型参数(如CustomEventArgs)天然支持 null 安全检查,编译器能捕获未初始化字段访问 - WPF / WinForms / MAUI 的数据绑定和设计器都依赖这个签名,换自定义委托会导致设计器无法识别事件
订阅时 += 不加 null 检查会出什么问题
常见写法 publisher.DataChanged += handler; 看似没问题,但若 handler 是实例方法,且该实例已被 GC 回收,而事件源仍持有引用,就会造成内存泄漏——这是 VisionPro 或长时间运行的工业检测程序里最常被忽略的坑。
- 必须在窗体关闭、控件销毁前显式调用
-=取消订阅,尤其在使用cogToolBlockEditV21.Subject.Ran这类第三方组件事件时 - 若 handler 是匿名方法或 lambda,且捕获了外部变量(比如局部
ListView控件),泄漏风险更高 - 推荐模式:在构造函数中订阅,在
Dispose()或FormClosing中统一取消,不要依赖 finalizer
CollectionChanged 事件为何不能直接在后台线程触发
ObservableCollection<t>.CollectionChanged</t> 事件本身是线程中立的,但它默认在**触发线程**上执行所有订阅者。当从 Timer 或 Task.Run 中修改集合时,CollectionChanged 的回调会跑在非 UI 线程,直接更新 TextBox.Text 或 ListBox.Items 就会抛 InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
- WPF 下必须用
Dispatcher.Invoke包裹 UI 更新逻辑 - WinForms 下检查
Control.InvokeRequired,再用Invoke或BeginInvoke - MAUI 中改用
MainThread.InvokeOnMainThreadAsync - 别试图用
SynchronizationContext手动捕获——它在异步链中断时不可靠
event 关键字到底封了什么
event 不是修饰符,它是个访问限制器:它把背后那个委托字段的赋值操作(=)给禁了,只允许外部用 += 和 -=。这意味着:
- 发布者内部仍可自由调用
MyEvent?.Invoke(...),但订阅者无法意外覆盖整个委托链(比如写成publisher.MyEvent = null) - 如果去掉
event,直接暴露委托字段,多个模块可能互相清空对方的订阅,导致事件静默失效 - 反模式示例:
public EventHandler<eventargs> MyEvent;</eventargs>—— 看似省事,实则埋雷
真正容易被忽略的,是事件触发时的空引用检查方式:MyEvent?.Invoke(this, e) 比 if (MyEvent != null) MyEvent(this, e) 更安全,因为多线程下后者存在竞态窗口:判断非空后,另一线程可能立刻把委托设为 null,再调用就炸了。










