1. 订阅事件的语法
想象一个任务管理应用:当任务完成时会触发一个事件,处理程序可以比如发送通知、更新 UI 或写日志。C# 让事件订阅既方便又安全:你明确表示哪个处理程序响应哪个事件。
在 C# 里订阅事件就像在聚会上把自己加入来宾名单:
publisher.MyEvent += HandlerMethod;
这里 publisher 是声明了事件 MyEvent 的对象,HandlerMethod 是在事件发生时会被调用的方法。
举个最小化的例子。假设我们有一个用于记录点击次数的程序:
public class Clicker
{
public event Action Clicked;
public void Click()
{
// 有东西被点击了!
Clicked?.Invoke();
}
}
这是我们的事件发布者 Clicked。现在来订阅一个处理程序:
Clicker clicker = new Clicker();
void OnClicked()
{
Console.WriteLine("按钮被按下了!");
}
clicker.Clicked += OnClicked;
// 在某处代码里
clicker.Click();
// → "按钮被按下了!"
它是怎么工作的?我们把方法 OnClicked 加到“点击”事件上,每次发生点击时它就会被调用。
2. 事件处理程序:有哪些以及如何声明
事件处理程序就是在事件触发时会被调用的方法。它的签名必须和事件使用的委托类型匹配。例如,如果事件被声明为 public event Action Clicked;, 那么处理程序应该是一个没有参数且没有返回值的方法。
用于 Action 的处理程序
void OnClicked()
{
Console.WriteLine("事件发生了 (Action)!");
}
用于标准 EventHandler 的处理程序
当你用传统的 EventHandler 时,处理程序接受两个参数:发送者(object sender)和事件数据(EventArgs e):
public class Alarm
{
public event EventHandler AlarmRaised;
public void RaiseAlarm()
{
AlarmRaised?.Invoke(this, EventArgs.Empty);
}
}
Alarm alarm = new Alarm();
void AlarmHandler(object sender, EventArgs e)
{
Console.WriteLine("警报触发了!");
}
alarm.AlarmRaised += AlarmHandler;
alarm.RaiseAlarm();
匿名方法和 lambda 表达式
C# 允许在订阅时直接使用匿名函数和 lambda 表达式:
clicker.Clicked += () => Console.WriteLine("另一次点击!");
如果事件带参数,可以写得稍复杂一点:
alarm.AlarmRaised += (sender, e) =>
{
Console.WriteLine($"Alarm raised by: {sender}");
};
4. 有用的细节
订阅和取消订阅:重要细节
现实情况是:聚会来得太多,或者有人想回家。对委托也是一样:处理程序可以添加(订阅)也可以移除(取消订阅):
// 订阅
publisher.MyEvent += MyHandler;
// 取消订阅(当处理程序不再需要时)
publisher.MyEvent -= MyHandler;
为啥重要?如果不取消订阅,尤其是在大型应用里,处理程序可能会一直“挂着”,阻止垃圾回收清理对象 —— 这会导致内存泄漏。
多个处理程序
一个事件可以有任意多个处理程序。它们会按添加顺序依次被调用。
clicker.Clicked += () => Console.WriteLine("第一个处理程序!");
clicker.Clicked += () => Console.WriteLine("第二个处理程序!");
clicker.Click();
// → 第一个处理程序!
// → 第二个处理程序!
你甚至可以在运行时随时订阅或取消订阅处理程序 — 很灵活。
这为什么很重要:真实场景
- UI (Windows Forms, WPF, WinUI, MAUI): 按钮点击、文本框内容变化——这些都是事件。
- FileSystemWatcher: 通知文件夹中新文件的出现。
- 异步操作: 文件下载完成、任务进度更新。
- 插件系统: 独立模块订阅主应用的事件。
事件模型让架构可扩展:你可以添加新模块并订阅现有事件,而不用改动核心代码。
订阅示例
| 事件签名 | 订阅示例 | 处理程序示例 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5. 常见错误和坑
错误 №1:处理程序签名不匹配。
如果事件声明为 event Action<int>,但你试图订阅一个无参数的方法,编译器会报错。始终检查方法是否符合事件要求的签名。
错误 №2:lambda 捕获变量。
通过 lambda 订阅时,它可能会捕获外部作用域的变量(见“闭包”主题)。如果在订阅后变量发生变化,处理程序会看到变化后的值,这可能导致意外行为。
错误 №3:订阅未初始化的事件源。
如果你在包含该事件的对象还没创建或初始化时就订阅它,可能会遇到 NullReferenceException。订阅前确保对象已准备好。
错误 №4:同一处理程序多次订阅。
如果同一个处理程序被多次订阅,它会被调用同样多次。这不是 bug,是事件的工作方式,但常常会让人意外。
GO TO FULL VERSION