必须使用标准事件模式,因它是.net事件互操作的隐性契约;需用eventhandler或其泛型变体,避免设计器、绑定及第三方库失效;事件参数须为eventargs或其子类且不可为null;推荐用protected virtual onxxx()封装触发逻辑。

标准事件模式不是可选项,而是.NET事件互操作的隐性契约——不用它,第三方控件、WPF绑定、WinForms设计器都可能拒绝配合。
为什么必须用 EventHandler 而不是自定义委托?
不是语法限制,而是生态兼容要求。.NET 框架层(如 Button.Click、Timer.Tick)和 UI 设计器生成的代码,全部硬编码依赖 EventHandler 或其泛型变体 EventHandler<teventargs></teventargs>。如果你写:
public delegate void MyFileFoundHandler(object sender, string fileName); public event MyFileFoundHandler FileFound;
会导致以下问题:
- Visual Studio 属性窗口里看不到该事件,无法双击生成处理方法
- WPF 的
{Binding ElementName=btn, Path=Click}类绑定失败 - 第三方库(如 ReactiveUI、Prism)的事件自动订阅机制失效
- 静态分析工具(如 Roslyn 分析器)报
CA1009(事件参数应遵循标准)
EventArgs 不能为空,但可以是 EventArgs.Empty
即使事件不传任何数据,也必须提供 EventArgs 参数。直接传 null 不仅违反约定,还会在某些调试场景(如 WinForms 设计器加载时)抛出 NullReferenceException。
正确做法是:
- 无数据 → 用
EventArgs.Empty:MyEvent?.Invoke(this, EventArgs.Empty) - 有数据 → 继承
EventArgs,且字段/属性设为只读(避免多订阅者间状态污染):public class FileFoundArgs : EventArgs { public string FoundFile { get; } } - 不要用
struct做事件参数 ——EventArgs是引用类型,设计上就要求所有监听器看到同一实例
泛型 EventHandler<t></t> 是推荐写法,但别滥用
EventHandler<filefoundargs></filefoundargs> 比手写委托更安全、更简洁,编译器自动做类型检查。但它不是万能的:
- 如果事件参数类型会在运行时动态变化(比如插件系统),泛型会限制灵活性,此时仍需回退到非泛型
EventHandler+ 类型转换 - 不要为每个小字段都建新
EventArgs子类 —— 比如只传一个int,用EventArgs+EventArgs.Empty配合外部状态更轻量 -
EventHandler<object></object>是反模式 —— 它放弃类型安全,等价于手动转型,失去泛型意义
触发事件前必须判空,且推荐用保护方法封装
直接写 MyEvent?.Invoke(...) 看似简单,但容易漏掉线程安全或重复触发逻辑。标准做法是加一层受保护的虚方法:
protected virtual void OnFileFound(FileFoundArgs e)
{
FileFound?.Invoke(this, e);
}
这样做的实际好处:
- 派生类可 override
OnFileFound插入日志、过滤、异步包装等逻辑 - 避免在多个地方重复写
if (FileFound != null) - 单元测试时可 mock
OnFileFound而不真正触发事件链 - WPF / WinForms 控件基类(如
Control)全部采用此模式,保持行为一致
最容易被忽略的一点:事件触发是同步的,且默认在调用线程执行。如果事件处理器里有耗时操作(如 IO、UI 更新),必须由订阅者自己决定是否 await 或切线程 —— 发布者不该替订阅者做这个决策。










