1. 介绍
所以,当我们在 C# 上下文里说到“事件”(event),指的是一种机制,可以安全地通知一个或多个对象某件事发生了。事件不是魔法:底层是 delegate(关键字 delegate),外面加了防止误用的保护。
想象订阅一个 YouTube 频道。频道(事件拥有者对象)有一个“订阅”按钮——任何观众(另一个对象)都可以点它并加入订阅者列表(delegate 的处理器)。当作者发布新视频(触发事件)时,只有订阅者会收到通知,而且只有频道作者决定什么时候触发。重要的一点:观众可以订阅或取消订阅,但不能自己发送通知——只有频道拥有者可以触发事件。
一些正式的语法
// 声明一个 delegate,用来定义事件处理器的签名
public delegate void SimpleEventHandler();
// 基于 delegate 声明 event
public event SimpleEventHandler SomethingHappened;
很简单:event 是用来声明事件的关键字,事件的类型总是由 delegate 指定。
2. 第一个简单例子:事件“按钮被按下!”
步骤 1. 为事件处理器定义 delegate
// 事件处理器:不返回值,不接受参数
public delegate void ButtonClickHandler();
步骤 2. 带事件的类
public class Button
{
// 事件:可以订阅和取消订阅,但不能从外部直接触发
public event ButtonClickHandler Click;
// 方法,表示“按下按钮”
public void Press()
{
Console.WriteLine("按钮被按下!");
// 触发事件:通知所有订阅者
Click?.Invoke();
}
}
注意:事件是基于 delegate 声明的,在 Press 方法里通过 ?.Invoke() 来调用事件。为什么要这样?因为事件可能没有订阅者,这时 Click 为 null。安全调用操作符保证只有在有订阅者时才会执行处理器。
步骤 3. 订阅事件并运行代码
// 使用示例
public class Program
{
public static void Main()
{
var button = new Button();
// 添加“监听器”(订阅事件)
button.Click += OnButtonClicked;
button.Click += () => Console.WriteLine("再来一个处理器!");
button.Press(); // 模拟按下按钮
// 可以取消订阅事件
button.Click -= OnButtonClicked;
button.Press();
}
// 普通的事件处理器
public static void OnButtonClicked()
{
Console.WriteLine("处理器:按钮被按下了!");
}
}
第一次按下会触发两个处理器,第二次按下只会触发 lambda。
3. event 和 delegate 的区别
对 delegate 可以自由赋值,甚至完全替换订阅列表——有人可能写 button.Click = null,这样之前的所有订阅都会被清掉。
event 就不一样了——它和对象绑定得更严格。只有声明事件的类的拥有者能直接触发它(比如在类内部调用 Click())。其他代码只能通过 += 订阅或通过 -= 取消订阅,但不能“一下子清空”所有订阅。这保护了封装,不让订阅机制被“弄坏”。
4. 签名:事件的 delegate 类型和参数
在 .NET 中的习惯是:事件处理器接受两个参数——object sender(谁触发了事件)和事件参数(EventArgs)。建议使用 EventHandler 或 EventHandler<TEventArgs>。
示例:带参数的 delegate
public delegate void ButtonClickHandler(object sender, EventArgs e);
EventArgs 是用于传递额外信息的基类。如果需要更多数据,就创建自己的子类。
把它应用到我们的按钮上
// 事件参数的类
public class ButtonClickEventArgs : EventArgs
{
public string UserName { get; }
public ButtonClickEventArgs(string userName)
{
UserName = userName;
}
}
public class Button
{
public event EventHandler<ButtonClickEventArgs> Click;
public void Press(string userName)
{
Console.WriteLine("按钮被按下!");
Click?.Invoke(this, new ButtonClickEventArgs(userName));
}
}
public static void Main()
{
var button = new Button();
button.Click += OnButtonClicked;
button.Press("瓦西里");
}
public static void OnButtonClicked(object sender, ButtonClickEventArgs e)
{
Console.WriteLine($"用户 {e.UserName} 按下了按钮!");
}
5. 可视化示意:事件如何工作
+-------------+
| 用户 |
+-------------+
|
v
+--------------+
| Button.Press|
+--------------+
|
v
+-----------------+ +------------------------+
| 调用 Click? |----->---| 订阅者 1 |
+-----------------+ +------------------------+
| | 执行处理器 |
v +------------------------+
+-----------------+ +------------------------+
| 如果有订阅者 |----->---| 订阅者 2 |
+-----------------+ +------------------------+
: | 执行处理器 |
v +------------------------+
— Button.Press 触发事件;订阅者的处理器按顺序执行。
6. 有用的细节
在 .NET 中推荐的事件形式
使用 EventHandler 和 EventHandler<TEventArgs>,这样代码更容易和库、工具互操作。这个做法方便事件演进:你可以在自己的 EventArgs 里添加新属性,而不会破坏现有订阅者。
文档: EventHandler, event.
事件 vs delegate
如果你只想把一个组件和一个具体方法绑定,delegate 就够了(delegate)。如果你需要不同部分的程序可以随时订阅和取消订阅,就用 event(event)。
7. 常见错误和尴尬点
错误 №1:在没有检查 null 的情况下调用事件。 如果事件没有订阅者,直接调用会导致 NullReferenceException。使用安全调用:
Click?.Invoke(...);
错误 №2:试图在类外部触发事件。 事件只能在声明它的类内部被触发。其他类尝试触发会被编译器拒绝——这是对封装的保护。
错误 №3:同一个处理器被多次订阅。 如果同一个处理器被订阅多次,它会被调用相应的次数。这是订阅机制的特性。注意不要重复使用 +=,并正确使用 -= 取消订阅。
GO TO FULL VERSION