C# 中事件的定义、使用和注意事项
一、什么是事件
事件,英文叫 event。
在 C# 中,事件可以理解为:
当某件事情发生时,通知其他对象来执行对应的代码。
比如:
- 按钮被点击了
- 文件下载完成了
- 用户登录成功了
- 角色血量变成 0 了
- 订单支付成功了
- 定时器时间到了
这些都可以看成“事件发生了”。
生活中也有类似的例子:
门铃响了,屋里的人听到后去开门。
门铃是事件源,响铃是事件,开门的人是事件处理者。
在程序中也是类似:
某个对象负责“发生事件”,其他对象负责“订阅事件”,一旦事件发生,订阅者就会收到通知并执行代码。
二、为什么需要事件
假设我们有一个下载器,下载完成后要提醒用户。
一种简单写法是直接在下载器里面写提示逻辑:
class Downloader
{
public void Download()
{
Console.WriteLine("开始下载...");
Console.WriteLine("下载完成");
Console.WriteLine("弹出提示:下载完成了");
}
}
这段代码能运行,但问题是:
- 下载器和提示逻辑绑得太死。
- 如果以后不想弹窗,而是写日志、发通知、刷新界面,就要修改
Downloader类。 - 如果有多个地方都想知道“下载完成了”,代码会越来越乱。
使用事件可以让代码更灵活:
class Downloader
{
public event Action DownloadCompleted;
public void Download()
{
Console.WriteLine("开始下载...");
Console.WriteLine("下载完成");
DownloadCompleted?.Invoke();
}
}
使用时:
Downloader downloader = new Downloader();
downloader.DownloadCompleted += () =>
{
Console.WriteLine("收到通知:下载完成了");
};
downloader.Download();
这样 Downloader 只负责下载和发出通知,至于通知之后做什么,由外部订阅者决定。
这就是事件的重要作用:
事件可以降低对象之间的耦合,让“发生事情的一方”和“处理事情的一方”分开。
三、事件中的几个角色
学习事件时,可以先记住三个角色。
| 角色 | 说明 | 例子 |
|---|---|---|
| 事件发布者 | 负责定义事件、触发事件的对象 | 按钮、下载器、定时器 |
| 事件订阅者 | 关注事件、处理事件的对象 | 界面、日志模块、业务模块 |
| 事件处理方法 | 事件发生时执行的方法 | 点击按钮后执行的方法 |
例如:
button.Click += Button_Click;
这里:
button是事件发布者。Click是事件。Button_Click是事件处理方法。+=表示订阅事件。
四、事件和委托的关系
在 C# 中,事件是基于委托实现的。
简单理解:
委托规定了“事件处理方法长什么样”,事件负责“保存和通知这些方法”。
先看一个委托:
public delegate void MyDelegate();
这个委托表示:
可以指向一个没有参数、没有返回值的方法。
然后可以基于这个委托声明事件:
public event MyDelegate MyEvent;
完整示例:
using System;
class Alarm
{
public delegate void AlarmHandler();
public event AlarmHandler Ring;
public void Start()
{
Console.WriteLine("闹钟开始工作");
Ring?.Invoke();
}
}
class Program
{
static void Main()
{
Alarm alarm = new Alarm();
alarm.Ring += WakeUp;
alarm.Start();
}
static void WakeUp()
{
Console.WriteLine("起床了");
}
}
输出:
闹钟开始工作
起床了
这里的关键点:
AlarmHandler是委托类型。Ring是事件。WakeUp是事件处理方法。alarm.Ring += WakeUp;表示订阅事件。Ring?.Invoke();表示触发事件。
五、事件的基本语法
事件声明的一般格式:
访问修饰符 event 委托类型 事件名;
例如:
public event Action OnStart;
或者:
public delegate void StartedHandler();
public event StartedHandler Started;
常见写法有两种:
1. 使用自定义委托
public delegate void MessageHandler(string message);
public event MessageHandler MessageReceived;
触发事件:
MessageReceived?.Invoke("你好");
订阅事件:
obj.MessageReceived += message =>
{
Console.WriteLine(message);
};
2. 使用系统内置委托
C# 已经提供了很多常用委托,例如:
ActionAction<T>Func<T>EventHandlerEventHandler<TEventArgs>
所以很多时候不需要自己声明委托。
例如:
public event Action<string> MessageReceived;
触发:
MessageReceived?.Invoke("你好");
六、事件的完整使用步骤
定义和使用事件,一般有四步。
第一步:定义事件
class Publisher
{
public event Action SomethingHappened;
}
第二步:触发事件
class Publisher
{
public event Action SomethingHappened;
public void DoSomething()
{
Console.WriteLine("正在做事情...");
SomethingHappened?.Invoke();
}
}
这里的:
SomethingHappened?.Invoke();
表示:
如果有人订阅了这个事件,就通知他们;如果没人订阅,就什么也不做。
第三步:订阅事件
Publisher publisher = new Publisher();
publisher.SomethingHappened += () =>
{
Console.WriteLine("收到通知:事情发生了");
};
+= 表示添加一个事件处理方法。
第四步:执行触发事件的方法
publisher.DoSomething();
完整代码:
using System;
class Publisher
{
public event Action SomethingHappened;
public void DoSomething()
{
Console.WriteLine("正在做事情...");
SomethingHappened?.Invoke();
}
}
class Program
{
static void Main()
{
Publisher publisher = new Publisher();
publisher.SomethingHappened += () =>
{
Console.WriteLine("收到通知:事情发生了");
};
publisher.DoSomething();
}
}
输出:
正在做事情...
收到通知:事情发生了
七、事件处理方法的写法
事件处理方法可以有几种常见写法。
1. 使用普通方法
publisher.SomethingHappened += HandleSomethingHappened;
static void HandleSomethingHappened()
{
Console.WriteLine("事件被处理了");
}
这种写法适合逻辑较多、需要复用、需要取消订阅的情况。
2. 使用匿名函数
publisher.SomethingHappened += () =>
{
Console.WriteLine("事件被处理了");
};
这种写法适合逻辑很短、只在这里使用的情况。
3. 使用 Lambda 表达式
如果事件带参数:
public event Action<string> MessageReceived;
订阅时:
publisher.MessageReceived += message =>
{
Console.WriteLine("收到消息:" + message);
};
触发时:
MessageReceived?.Invoke("你好,事件");
八、带参数的事件
很多事件发生时,需要把一些信息传给订阅者。
例如用户登录成功后,需要告诉外部“是谁登录了”。
using System;
class UserService
{
public event Action<string> UserLoggedIn;
public void Login(string userName)
{
Console.WriteLine($"{userName} 正在登录...");
Console.WriteLine($"{userName} 登录成功");
UserLoggedIn?.Invoke(userName);
}
}
class Program
{
static void Main()
{
UserService userService = new UserService();
userService.UserLoggedIn += name =>
{
Console.WriteLine("日志记录:用户登录成功,用户名是 " + name);
};
userService.UserLoggedIn += name =>
{
Console.WriteLine("欢迎回来," + name);
};
userService.Login("小明");
}
}
输出:
小明 正在登录...
小明 登录成功
日志记录:用户登录成功,用户名是 小明
欢迎回来,小明
这个例子说明:
- 一个事件可以有多个订阅者。
- 事件触发时,会依次调用所有订阅者。
- 事件参数可以把有用的信息传出去。
九、使用 EventHandler 的标准事件写法
在 .NET 中,事件有一种很常见、很标准的写法:
public event EventHandler SomeEvent;
EventHandler 表示事件处理方法通常长这样:
void 方法名(object sender, EventArgs e)
其中:
sender表示是谁触发了事件。e表示事件相关的数据。
1. 不带额外数据的标准事件
using System;
class Door
{
public event EventHandler Opened;
public void Open()
{
Console.WriteLine("门打开了");
Opened?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Door door = new Door();
door.Opened += Door_Opened;
door.Open();
}
static void Door_Opened(object sender, EventArgs e)
{
Console.WriteLine("收到通知:门已经打开");
}
}
这里的:
Opened?.Invoke(this, EventArgs.Empty);
表示触发事件。
this 表示当前这个 Door 对象就是事件发送者。
EventArgs.Empty 表示没有额外数据。
2. 带额外数据的标准事件
如果事件需要传更多信息,可以自定义一个继承自 EventArgs 的类。
例如订单支付成功事件,需要传订单编号和金额。
using System;
class OrderPaidEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Amount { get; }
public OrderPaidEventArgs(string orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
class OrderService
{
public event EventHandler<OrderPaidEventArgs> OrderPaid;
public void Pay(string orderId, decimal amount)
{
Console.WriteLine($"订单 {orderId} 支付成功,金额 {amount}");
OrderPaid?.Invoke(this, new OrderPaidEventArgs(orderId, amount));
}
}
class Program
{
static void Main()
{
OrderService orderService = new OrderService();
orderService.OrderPaid += OrderService_OrderPaid;
orderService.Pay("A001", 99.9m);
}
static void OrderService_OrderPaid(object sender, OrderPaidEventArgs e)
{
Console.WriteLine($"收到订单支付事件:订单号 {e.OrderId},金额 {e.Amount}");
}
}
输出:
订单 A001 支付成功,金额 99.9
收到订单支付事件:订单号 A001,金额 99.9
这是 .NET 中非常推荐的事件写法。
十、自定义事件和标准事件的选择
初学时可能会疑惑:
到底应该用
Action,还是用EventHandler?
可以这样理解:
| 写法 | 适合场景 |
|---|---|
event Action |
简单事件,不需要参数 |
event Action<T> |
简单事件,需要传少量数据 |
event EventHandler |
标准事件,不需要额外数据 |
event EventHandler<TEventArgs> |
标准事件,需要传事件数据 |
实际项目中,如果是公开 API、类库、框架代码,更推荐使用 EventHandler 或 EventHandler<TEventArgs>。
十一、事件的订阅和取消订阅
1. 订阅事件:+=
publisher.SomethingHappened += HandleSomething;
意思是:
当
SomethingHappened发生时,请调用HandleSomething。
2. 取消订阅事件:-=
publisher.SomethingHappened -= HandleSomething;
意思是:
以后这个事件发生时,不要再调用
HandleSomething。
完整示例:
using System;
class TimerLike
{
public event Action Tick;
public void Run()
{
Tick?.Invoke();
}
}
class Program
{
static void Main()
{
TimerLike timer = new TimerLike();
timer.Tick += OnTick;
timer.Run(); // 会执行 OnTick
timer.Tick -= OnTick;
timer.Run(); // 不会执行 OnTick
}
static void OnTick()
{
Console.WriteLine("时间到了");
}
}
输出:
时间到了
3. 匿名函数取消订阅的问题
下面这种写法可以订阅:
timer.Tick += () =>
{
Console.WriteLine("时间到了");
};
但是下面这种写法通常不能取消前面的订阅:
timer.Tick -= () =>
{
Console.WriteLine("时间到了");
};
原因是:
两段看起来一样的匿名函数,其实是两个不同的函数对象。
如果以后需要取消订阅,应该先把匿名函数保存到变量中:
Action handler = () =>
{
Console.WriteLine("时间到了");
};
timer.Tick += handler;
// 以后需要取消时
timer.Tick -= handler;
十二、事件可以有多个订阅者
一个事件可以被多个方法订阅。
using System;
class Button
{
public event Action Click;
public void Press()
{
Console.WriteLine("按钮被按下");
Click?.Invoke();
}
}
class Program
{
static void Main()
{
Button button = new Button();
button.Click += ShowMessage;
button.Click += WriteLog;
button.Click += RefreshPage;
button.Press();
}
static void ShowMessage()
{
Console.WriteLine("显示提示信息");
}
static void WriteLog()
{
Console.WriteLine("记录日志");
}
static void RefreshPage()
{
Console.WriteLine("刷新页面");
}
}
输出:
按钮被按下
显示提示信息
记录日志
刷新页面
这说明事件很适合“一件事发生后,多个地方都要做出反应”的场景。
十三、事件只能由声明它的类触发
事件和普通委托很像,但事件有一个重要限制:
在类的外部,只能订阅和取消订阅事件,不能直接触发事件。
例如:
class Publisher
{
public event Action SomethingHappened;
public void RaiseEvent()
{
SomethingHappened?.Invoke();
}
}
外部可以这样写:
publisher.SomethingHappened += Handler;
publisher.SomethingHappened -= Handler;
但不能这样写:
// 错误:外部不能直接触发事件
publisher.SomethingHappened?.Invoke();
这个限制很重要。
它保证了:
事件什么时候发生,只能由事件发布者自己决定。
如果不用 event,只暴露一个普通委托字段:
public Action SomethingHappened;
外部就可以随便调用、清空、覆盖它:
publisher.SomethingHappened = null;
publisher.SomethingHappened?.Invoke();
这会破坏封装。
所以事件比直接暴露委托更安全。
十四、事件触发时为什么常用 ?.Invoke()
触发事件时,经常看到这种写法:
SomethingHappened?.Invoke();
它等价于:
if (SomethingHappened != null)
{
SomethingHappened();
}
如果没人订阅事件,事件就是 null。
直接调用会报错:
// 如果没人订阅,会出现 NullReferenceException
SomethingHappened();
所以要先判断是否为空。
?.Invoke() 是更简洁、更常见的写法。
带参数时:
MessageReceived?.Invoke("你好");
标准事件时:
Opened?.Invoke(this, EventArgs.Empty);
十五、建议封装触发事件的方法
在较规范的代码中,通常会把触发事件的逻辑封装到一个 On事件名 方法中。
例如:
class Door
{
public event EventHandler Opened;
public void Open()
{
Console.WriteLine("门打开了");
OnOpened();
}
protected virtual void OnOpened()
{
Opened?.Invoke(this, EventArgs.Empty);
}
}
为什么这样写?
- 触发事件的逻辑集中在一个方法里。
- 子类可以重写
OnOpened,扩展行为。 - 更符合 .NET 的常见编码习惯。
如果带参数:
protected virtual void OnOrderPaid(OrderPaidEventArgs e)
{
OrderPaid?.Invoke(this, e);
}
然后调用:
OnOrderPaid(new OrderPaidEventArgs(orderId, amount));
十六、事件和普通方法调用的区别
普通方法调用:
SendMessage();
特点:
- 调用者明确知道要调用谁。
- 通常是一对一调用。
- 关系比较直接。
事件调用:
MessageReceived?.Invoke();
特点:
- 发布者不知道具体谁在处理。
- 可以有多个订阅者。
- 发布者和订阅者之间关系更松散。
对比:
| 对比项 | 普通方法调用 | 事件 |
|---|---|---|
| 调用关系 | 调用者知道被调用者 | 发布者不知道订阅者 |
| 数量 | 通常一对一 | 可以一对多 |
| 耦合度 | 较高 | 较低 |
| 适合场景 | 明确调用某个功能 | 某件事发生后通知外部 |
十七、事件和委托字段的区别
很多初学者会觉得事件和委托字段很像。
例如:
public Action OnChanged;
和:
public event Action Changed;
它们确实有关系,但不一样。
普通委托字段的问题
class Counter
{
public Action Changed;
}
外部可以这样做:
counter.Changed = null;
counter.Changed = SomeOtherMethod;
counter.Changed?.Invoke();
也就是说,外部可以:
- 覆盖所有订阅者。
- 清空所有订阅者。
- 在类外部随便触发。
这不安全。
使用事件更安全
class Counter
{
public event Action Changed;
}
外部只能:
counter.Changed += Handler;
counter.Changed -= Handler;
不能直接:
counter.Changed = null;
counter.Changed?.Invoke();
所以:
对外通知时,应该优先使用
event,不要直接暴露 public 委托字段。
十八、事件的命名习惯
C# 中事件命名通常使用 PascalCase,也就是每个单词首字母大写。
常见事件名:
Clicked
Opened
Closed
Changed
Completed
Failed
UserLoggedIn
OrderPaid
事件处理方法常见命名:
Button_Click
Door_Opened
OrderService_OrderPaid
触发事件的方法常见命名:
OnClicked()
OnOpened()
OnOrderPaid()
事件参数类通常以 EventArgs 结尾:
OrderPaidEventArgs
UserLoggedInEventArgs
FileDownloadedEventArgs
十九、常见使用场景
1. UI 控件事件
WinForms 或 WPF 中,按钮点击就是典型事件。
button1.Click += (sender, e) =>
{
MessageBox.Show("按钮被点击了");
};
这里:
button1是事件发布者。Click是事件。- Lambda 是事件处理方法。
2. 业务状态变化通知
例如用户登录成功:
public event EventHandler<UserLoggedInEventArgs> UserLoggedIn;
登录成功后触发:
UserLoggedIn?.Invoke(this, new UserLoggedInEventArgs(userName));
3. 进度通知
例如文件下载进度变化:
public event Action<int> ProgressChanged;
public void Download()
{
for (int progress = 0; progress <= 100; progress += 10)
{
ProgressChanged?.Invoke(progress);
}
}
订阅:
downloader.ProgressChanged += progress =>
{
Console.WriteLine($"当前进度:{progress}%");
};
4. 定时器
timer.Tick += (sender, e) =>
{
Console.WriteLine("定时器触发");
};
5. 游戏开发
例如角色死亡:
public event Action PlayerDied;
public void TakeDamage(int damage)
{
hp -= damage;
if (hp <= 0)
{
PlayerDied?.Invoke();
}
}
订阅:
player.PlayerDied += () =>
{
Console.WriteLine("播放死亡动画");
};
player.PlayerDied += () =>
{
Console.WriteLine("显示失败界面");
};
二十、事件的注意事项
1. 触发事件前要判断是否有人订阅
推荐:
Changed?.Invoke();
不推荐:
Changed();
如果没人订阅,直接调用会报空引用异常。
2. 不要在类外部直接控制事件触发
事件应该由声明它的类自己触发。
外部对象只负责订阅和取消订阅。
这样可以保持封装性。
3. 长时间存在的事件要注意取消订阅
如果一个短生命周期对象订阅了一个长生命周期对象的事件,却没有取消订阅,可能导致短生命周期对象无法被垃圾回收。
例如:
globalService.Changed += page.HandleChanged;
如果 globalService 一直存在,而 page 已经不需要了,但没有取消订阅,那么 globalService 仍然持有 page 的引用。
更稳妥的做法:
globalService.Changed -= page.HandleChanged;
在 UI 页面关闭、对象释放、服务停止时,要特别注意取消订阅。
4. 匿名函数订阅后不方便取消
下面写法很简洁:
button.Click += (sender, e) =>
{
Console.WriteLine("点击了按钮");
};
但如果以后需要取消订阅,就不方便。
如果需要取消订阅,请保存事件处理器:
EventHandler handler = (sender, e) =>
{
Console.WriteLine("点击了按钮");
};
button.Click += handler;
button.Click -= handler;
5. 事件处理方法中不要写过重逻辑
事件触发后,所有订阅者通常会依次执行。
如果某个事件处理方法执行很慢,可能会影响整个流程。
例如:
button.Click += (sender, e) =>
{
// 不推荐在 UI 事件里直接执行很耗时的操作
LongTimeWork();
};
更好的做法是:
button.Click += async (sender, e) =>
{
await LongTimeWorkAsync();
};
尤其是在 UI 程序中,耗时操作容易导致界面卡住。
6. 事件处理方法中出现异常要小心
如果一个事件有多个订阅者,其中一个订阅者抛出异常,后面的订阅者可能不会继续执行。
例如:
publisher.Changed += Handler1;
publisher.Changed += Handler2;
publisher.Changed += Handler3;
如果 Handler2 抛出异常,Handler3 可能无法执行。
如果事件很关键,可以在触发时分别调用每个订阅者,并单独处理异常。
示例:
var handlers = Changed?.GetInvocationList();
if (handlers != null)
{
foreach (Action handler in handlers)
{
try
{
handler();
}
catch (Exception ex)
{
Console.WriteLine("事件处理出错:" + ex.Message);
}
}
}
初学阶段不必一开始就写这么复杂,但要知道:
多个事件处理方法是依次执行的,一个方法出错可能影响后面的处理。
7. 多线程环境下要注意线程安全
在简单程序中,使用:
Changed?.Invoke();
通常就够了。
在多线程环境中,有时会先复制一份事件引用:
var handler = Changed;
handler?.Invoke();
这样可以减少事件在检查和调用之间被其他线程修改带来的问题。
不过对于初学者来说,先掌握 ?.Invoke() 即可。
8. 异步事件处理要小心 async void
UI 事件中常见:
button.Click += async (sender, e) =>
{
await SaveAsync();
};
这种情况下事件处理器本质上接近 async void,在 UI 事件中是可以接受的。
但在普通业务代码中,尽量不要随便使用 async void,因为异常处理和流程控制会更困难。
如果自己设计异步通知机制,可以考虑使用返回 Task 的委托:
public event Func<Task> SavedAsync;
不过事件本身对异步支持没有普通方法调用那么直接,所以初学阶段先了解即可。
9. 不要滥用事件
事件适合“某件事发生后通知外部”。
如果只是普通的一步调用,比如“保存用户”“计算价格”“发送短信”,直接用方法可能更清楚。
不推荐为了使用事件而使用事件。
可以这样判断:
- 如果调用者明确知道要调用谁,用方法。
- 如果发布者只负责通知,不关心谁处理,用事件。
- 如果一件事发生后可能有多个对象响应,用事件。
10. 不要把事件当成返回结果的工具
事件通常是通知机制,不适合用来获取返回值。
例如不建议:
// 不推荐:试图通过事件向外部要一个计算结果
public event Func<int> NeedNumber;
如果你需要一个明确的返回值,通常更适合用方法、接口或回调。
二十一、课堂讲解建议
讲事件时,不建议一开始就讲复杂的 EventHandler<TEventArgs>。
可以按这个顺序讲:
- 先讲生活例子:门铃响了,有人去开门。
- 再讲程序例子:按钮点击了,执行点击方法。
- 再用
Action写一个最简单事件。 - 然后讲
+=是订阅,-=是取消订阅。 - 再讲
?.Invoke()是触发事件。 - 最后讲 .NET 标准写法
EventHandler。
学生最容易混淆的地方有三个:
- 事件不是方法,事件是“通知机制”。
+=不是执行事件,而是订阅事件。?.Invoke()才是触发事件。
可以用下面这句话帮助理解:
订阅事件就像留下电话号码,事件发生时,发布者会按名单通知所有订阅者。
二十二、完整示例:商品库存不足事件
下面通过一个完整例子,把事件的定义、订阅、触发、传参都串起来。
using System;
class StockLowEventArgs : EventArgs
{
public string ProductName { get; }
public int CurrentStock { get; }
public StockLowEventArgs(string productName, int currentStock)
{
ProductName = productName;
CurrentStock = currentStock;
}
}
class Product
{
public string Name { get; }
public int Stock { get; private set; }
public event EventHandler<StockLowEventArgs> StockLow;
public Product(string name, int stock)
{
Name = name;
Stock = stock;
}
public void Sell(int count)
{
if (count <= 0)
{
Console.WriteLine("销售数量必须大于 0");
return;
}
if (count > Stock)
{
Console.WriteLine("库存不足,无法销售");
return;
}
Stock -= count;
Console.WriteLine($"卖出 {count} 件 {Name},当前库存 {Stock}");
if (Stock < 5)
{
OnStockLow(new StockLowEventArgs(Name, Stock));
}
}
protected virtual void OnStockLow(StockLowEventArgs e)
{
StockLow?.Invoke(this, e);
}
}
class Program
{
static void Main()
{
Product product = new Product("键盘", 10);
product.StockLow += Product_StockLow;
product.Sell(3);
product.Sell(3);
product.Sell(2);
}
static void Product_StockLow(object sender, StockLowEventArgs e)
{
Console.WriteLine($"库存预警:{e.ProductName} 当前只剩 {e.CurrentStock} 件");
}
}
输出:
卖出 3 件 键盘,当前库存 7
卖出 3 件 键盘,当前库存 4
库存预警:键盘 当前只剩 4 件
卖出 2 件 键盘,当前库存 2
库存预警:键盘 当前只剩 2 件
这个例子中:
Product是事件发布者。StockLow是事件。StockLowEventArgs是事件数据。Product_StockLow是事件处理方法。product.StockLow += Product_StockLow;是订阅事件。StockLow?.Invoke(this, e);是触发事件。
二十三、练习题
练习 1:定义一个简单事件
定义一个 Light 类,当调用 TurnOn() 方法时,触发 TurnedOn 事件。
参考答案:
using System;
class Light
{
public event Action TurnedOn;
public void TurnOn()
{
Console.WriteLine("灯打开了");
TurnedOn?.Invoke();
}
}
class Program
{
static void Main()
{
Light light = new Light();
light.TurnedOn += () =>
{
Console.WriteLine("收到通知:灯已经打开");
};
light.TurnOn();
}
}
练习 2:定义一个带参数的事件
定义一个 ScoreManager 类,当添加分数后,触发 ScoreChanged 事件,并传出当前分数。
参考答案:
using System;
class ScoreManager
{
public event Action<int> ScoreChanged;
public int Score { get; private set; }
public void AddScore(int value)
{
Score += value;
ScoreChanged?.Invoke(Score);
}
}
class Program
{
static void Main()
{
ScoreManager manager = new ScoreManager();
manager.ScoreChanged += score =>
{
Console.WriteLine("当前分数:" + score);
};
manager.AddScore(10);
manager.AddScore(20);
}
}
练习 3:使用标准 EventHandler
定义一个 DownloadService 类,当下载完成时触发 DownloadCompleted 事件。
参考答案:
using System;
class DownloadService
{
public event EventHandler DownloadCompleted;
public void Download()
{
Console.WriteLine("正在下载...");
Console.WriteLine("下载完成");
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
DownloadService service = new DownloadService();
service.DownloadCompleted += Service_DownloadCompleted;
service.Download();
}
static void Service_DownloadCompleted(object sender, EventArgs e)
{
Console.WriteLine("收到通知:下载已经完成");
}
}
练习 4:取消订阅事件
订阅一个事件后,再取消订阅,观察事件处理方法是否还会执行。
参考答案:
using System;
class Notifier
{
public event Action Notified;
public void Notify()
{
Notified?.Invoke();
}
}
class Program
{
static void Main()
{
Notifier notifier = new Notifier();
notifier.Notified += HandleNotify;
notifier.Notify();
notifier.Notified -= HandleNotify;
notifier.Notify();
}
static void HandleNotify()
{
Console.WriteLine("收到通知");
}
}
输出只有一次:
收到通知
二十四、总结
事件是 C# 中非常重要的机制,常用于 UI、业务通知、状态变化、进度更新、定时器和插件扩展等场景。
可以记住下面几句话:
- 事件表示“某件事情发生了”。
- 事件基于委托,委托决定事件处理方法的参数和返回值。
event关键字可以保护委托,避免外部随意触发或覆盖。+=表示订阅事件。-=表示取消订阅事件。?.Invoke()表示触发事件。- 一个事件可以有多个订阅者。
- 简单事件可以用
Action,标准事件推荐用EventHandler。 - 需要传事件数据时,可以使用
EventHandler<TEventArgs>。 - 长生命周期对象的事件要注意取消订阅,避免内存泄漏。
一句话概括:
C# 事件就是一种“对象之间的通知机制”:发布者只负责宣布事情发生了,订阅者自己决定听到通知后要做什么。