1. 介绍
先回忆一下:事件是一个公开的契约,承诺你的代码在某事发生时会“调用”订阅者。在之前的讲座里你可能见过这样的声明:
public event Action<string> MessageSent;
或者:
public delegate void MyHandler(int value);
public event MyHandler SomethingHappened;
这能工作,但这种做法会带来一堆问题——从处理器签名不统一到无法知道事件来自谁以及发生了什么。想象一下键盘每次按下都随机触发,不同场景不同结果,而还是在同一个程序里。比如你在玩游戏,按空格键,刚才是跳跃,现在却竟然打开了物品栏。噩梦般的不标准!
在 .NET 里有种事件“宪法”——这是一套关于处理器签名和随事件传递信息结构的约定。典型风格长这样:
void Handler(object sender, EventArgs args);
熟悉吧?从 WinForms 的按钮点击到 ASP.NET 的系统事件,甚至第三方库,你都会看到这行签名。
2. 什么是 EventHandler 和 EventArgs
核心思想:每个事件传递两件事:
- 谁触发了事件? sender
- 发生了什么? EventArgs — 附加数据
在 C# 里通常这样表达:
public delegate void EventHandler(object sender, EventArgs e);
- object sender — 指向事件发起者的引用。可以是任意对象,通常是 this。
- EventArgs e — 带事件额外信息的对象。简单场景用 EventArgs.Empty,复杂场景创建自定义继承自 EventArgs 的类型。
趣闻
在 .NET 的官方建议里,公共 API 的事件应采用签名 (object sender, EventArgs e)。如果你看到没有 sender 和 EventArgs 的事件,那很可能是作者简化了,而不是标准的 .NET 风格。
统一标准的巨大好处
有了统一标准,事件更容易记录、测试、以通用方式订阅并扩展系统。打开 WinForms、WPF、ASP.NET 的源码,你会看到相同的模板。
3. 如何使用标准事件模板
1. 使用内置委托 EventHandler
不必声明自己的 delegate,可以直接用现成的:
public event EventHandler SmthHappened;
现在处理器总是长得一样:
private void OnSmthHappened(object sender, EventArgs e)
{
// 对事件的响应逻辑
}
订阅仍然很简单:
myObj.SmthHappened += OnSmthHappened;
重要:如果事件不传递额外数据,使用 EventArgs.Empty。
2. 创建自定义事件参数
如果需要传递信息(计算结果、文件名、错误等),就创建一个继承自 EventArgs 的类型:
public class CalculationEventArgs : EventArgs
{
public double Result { get; }
public CalculationEventArgs(double result) => Result = result;
}
然后使用泛型委托 EventHandler<TEventArgs>:
public event EventHandler<CalculationEventArgs> CalculationFinished;
处理器现在接收你的具体参数类型:
private void OnCalculationFinished(object sender, CalculationEventArgs e)
{
Console.WriteLine($"计算完成。结果: {e.Result}");
}
4. 应用示例
扩展我们的教学项目——假设有个计算器,做加减运算,并通过事件通知操作完成。
最简示例
public class Calculator
{
public event EventHandler<CalculationEventArgs> CalculationPerformed;
public void Add(int a, int b)
{
int result = a + b;
// 触发事件
CalculationPerformed?.Invoke(this, new CalculationEventArgs(result));
}
}
public class CalculationEventArgs : EventArgs
{
public int Result { get; }
public CalculationEventArgs(int result) => Result = result;
}
订阅并使用:
var calc = new Calculator();
calc.CalculationPerformed += (sender, e) =>
{
Console.WriteLine($"操作结果: {e.Result}");
};
calc.Add(10, 20);
// 输出: 操作结果: 30
这就是“标准”:处理器总是接收发送者对象和参数对象——通用且清晰。
5. 有用的细节
封装触发事件的逻辑:好习惯
在 .NET 中通常把触发事件的逻辑放到一个受保护的方法,方法名前缀为 On:
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
在具体逻辑(比如 "Add", "Subtract")里只需调用这个方法:
public void Add(int a, int b) => OnCalculationPerformed(new CalculationEventArgs(a + b));
这种风格允许继承者重写事件触发行为,并减少忘记触发事件的风险。
示意图:按 .NET 标准事件是如何工作的
graph LR
A[发布对象] -- "event EventHandler/ EventHandler<TEventArgs>" --> B[订阅者列表]
B -- "处理方法 (object sender, EventArgs e)" --> C[对事件的响应]
A -- "this (发送者)" --> C
A -- "EventArgs (数据)" --> C
事件声明方式对比
| 方式 | 传递触发者 (sender) | 传递参数 | 通用性 | 在 .NET 中的使用 |
|---|---|---|---|---|
|
否 | 是 | 低 | 否 |
|
是 | 是 | 中等 | 否(很少) |
|
是 | 否 (EventArgs) | 高 | 是,标准 |
|
是 | 是 (MyArgs) | 非常高 | 是,标准 |
面试和生产代码中的实际价值
使用标准事件模板是 .NET 开发者的必备技能。面试时很可能会问到 EventHandler 和那对 sender/EventArgs。像 Action<T> 这样的事件通常被认为是“简化版”。
在真实项目中,这种做法便于协作、测试、扩展和维护。第三方库(日志、分析器)在遇到标准形式时也更容易集成。
事件触发流程的块状图
flowchart TD
subgraph A[发布类]
C1((对象))
C2[触发事件的方法]
C3["event EventHandler<MyArgs>"]
end
subgraph B[订阅类]
D1((订阅))
D2["事件处理器 (object sender, MyArgs args)"]
end
C1 -- 调用 --> C2
C2 -- "Invoke(this, args)" --> C3
C3 -- "通知" --> D1
D1 -- "执行" --> D2
6. 建议、细节和常见错误
1. 处理器签名。 想用 Action<int> 类型的事件?虽然诱人,但你会失去 sender 和与生态的一致性。
2. 参数传递。 不要把 EventArgs 和普通参数混淆。把所有传给处理器的数据放到事件参数对象里。
3. 用 null 作为参数。 不要用 null,如果没有额外数据就用 EventArgs.Empty。
4. 弱类型化。 不要把所有事件都用一个“万金油” EventHandler,把各类事件做成各自的子类参数——可读性和可靠性更高。
5. 触发事件时报错。 始终检查是否有订阅者: SomeEvent?.Invoke(this, e)。没有订阅者时事件引用为 null。
6. 破坏封装。 不要在发布类之外触发事件。事件只用于订阅/退订;触发应在类内部通过 On... 方法完成。
GO TO FULL VERSION