事件 (Events)
事件 (Event) 是 C# 中非常核心但也相对难以理解的概念。它是类与类之间通信的重要机制。
事件简介
什么是事件?
指某个类的对象在运行过程中,遇到了一些特定的事情。
- 这些特定的事情需要通知到对象的使用者(订阅者)。
- 当发生与某个事件相关的动作时,类就会使用事件将这个对象通知给用户。
- 这种通知的操作称为 引发事件 (Raising an Event)。
为什么说事件很难理解? 很多初学者容易混淆“委托”和“事件”。 简单来说:事件是基于委托的。如果把委托看作是“函数指针的容器”,那么事件就是对这个容器的一种“封装”和“保护”。
我们可以用生活中的例子来类比:
- 发布者 (Publisher):烧水壶。
- 订阅者 (Subscriber):你。
- 事件:水开了(特定的事情)。
- 处理方法:当水开了,水壶(发布者)发出鸣叫(通知),你(订阅者)听到后去关火(处理方法)。
委托的发布与订阅
在学习 `event` 关键字之前,我们先看看如何仅使用 委托 (Delegate) 来实现发布和订阅模式。这有助于理解事件的底层原理。
根据教程定义,通过委托实现事件处理的过程,通常需要以下 4 个步骤:
- (1) 定义委托类型,并在发布者类中定义一个该类型的公有成员。
- (2) 在订阅者类中定义委托处理方法。
- (3) 订阅者对象将其事件处理方法链接到发布者对象的委托成员(一个委托类型的引用)上。
- (4) 发布者对象在特定的情况下“激发”委托操作,从而自动调用订阅者对象的委托处理方法。
代码示例:使用纯委托
using System; namespace DelegateDemo { // 1. 定义委托类型 public delegate void NotifyEventHandler(); // 发布者类 (Publisher) public class Heater { // 1. 在发布者类中定义一个该类型的公有成员 (注意:这里是 public 字段) public NotifyEventHandler OnBoil; public void BoilWater() { Console.WriteLine("水壶:开始烧水..."); // 模拟烧水过程 System.Threading.Thread.Sleep(2000); // 4. 发布者在特定情况下“激发”委托 if (OnBoil != null) { Console.WriteLine("水壶:水开了!(激发委托)"); OnBoil(); // 调用委托列表中的所有方法 } } } // 订阅者类 (Subscriber) public class Alarm { // 2. 在订阅者类中定义委托处理方法 public void MakeAlert() { Console.WriteLine("报警器:滴滴滴!水开了!"); } } class Program { static void Main(string[] args) { Heater heater = new Heater(); Alarm alarm = new Alarm(); // 3. 订阅者将处理方法链接到发布者的委托成员上 heater.OnBoil += alarm.MakeAlert; // 开始运行 heater.BoilWater(); } } }
纯委托模式的缺陷: 在上面的代码中,`OnBoil` 是一个 `public` 字段。这意味着外部类不仅可以订阅 (`+=`),还可以:
- 直接赋值 (`heater.OnBoil = null;`) —— 这会清空所有其他人的订阅!
- 直接调用 (`heater.OnBoil();`) —— 即使水没开,外部也能伪造事件!
这破坏了封装性,因此我们需要 事件 (Event)。
事件的发布与订阅
为了解决纯委托暴露出的安全问题,C# 引入了 `event` 关键字。 事件本质上是对委托的封装。它限制了外部类对委托的操作权限。
事件的特性:
- 在定义事件的类内部:可以像普通委托一样调用(引发事件)。
- 在定义事件的类外部:只能使用 `+=` (订阅) 和 `-=` (取消订阅),不能使用 `=` (赋值) 或直接调用。
代码示例:使用 event 关键字
我们将代码稍作修改,只需加上 `event` 关键字。
using System; namespace EventDemo { // 定义委托类型 public delegate void NotifyEventHandler(); public class Heater { // 【关键修改】这里加上了 event 关键字 // 虽然它还是 public,但编译器会对它进行保护 public event NotifyEventHandler OnBoil; public void BoilWater() { Console.WriteLine("水壶:开始烧水..."); // 只有在类的内容,才能调用这个事件 if (OnBoil != null) { OnBoil(); } } } public class Alarm { public void MakeAlert() { Console.WriteLine("报警器:滴滴滴!"); } } class Program { static void Main(string[] args) { Heater heater = new Heater(); Alarm alarm = new Alarm(); // 订阅事件 (合法) heater.OnBoil += alarm.MakeAlert; // heater.OnBoil = null; // 【报错】无法在外部直接赋值 // heater.OnBoil(); // 【报错】无法在外部直接调用 heater.BoilWater(); } } }
总结
| 特性 | 公有委托字段 (Delegate) | 事件 (Event) |
| 定义方式 | `public DelegateType Name;` | `public event DelegateType Name;` |
| 外部订阅 | 支持 `+=` 和 `-=` | 支持 `+=` 和 `-=` |
| 外部赋值 | 支持 `=` (危险,会覆盖他人) | 禁止 (编译错误) |
| 外部调用 | 支持 `Name()` (危险,可伪造) | 禁止 (编译错误) |
| 安全性 | 低 | 高 (封装性好) |
使用 `event` 关键字,就是为了保证“发布者负责触发,订阅者负责处理,除此之外谁也不能乱动”的契约。
评论