1. 什么是 C# 事件?
在 C# 中,**事件(Event)**是一种特殊的委托类型成员,用于实现发布-订阅(Publisher-Subscriber)模式。它允许一个对象(发布者)在特定动作发生时通知其他对象(订阅者),从而实现对象间的松耦合通信。
事件的核心思想是:"发生了某件事"。例如,按钮被点击、文件下载完成、温度超过阈值等。事件的典型应用场景包括 GUI 编程(如 WinForms、WPF)、异步通知、插件系统等。
2. 事件的核心要素
一个完整的事件机制包含以下三个关键部分:
- 事件发布者(Publisher):定义事件并触发事件的对象。
- 事件订阅者(Subscriber):注册事件处理程序并响应事件的对象。
- 事件处理程序(EventHandler):当事件被触发时执行的方法。
3. 声明与使用事件的语法
3.1 使用内置 EventHandler 委托
这是最常用、最标准的方式。.NET 提供了 EventHandler 和 EventHandler<TEventArgs> 委托。
csharp
// 发布者类
public class Button
{
// 1. 声明事件(使用 EventHandler 委托)
public event EventHandler Clicked;
// 2. 触发事件的方法(通常命名为 OnXXX,并遵循 protected virtual 约定)
protected virtual void OnClicked(EventArgs e)
{
// 使用空条件运算符安全调用
Clicked?.Invoke(this, e);
}
// 模拟按钮被点击
public void SimulateClick()
{
Console.WriteLine("按钮被点击了!");
OnClicked(EventArgs.Empty);
}
}
// 订阅者类
public class Logger
{
// 事件处理程序:签名必须匹配 EventHandler(object sender, EventArgs e)
public void LogButtonClick(object sender, EventArgs e)
{
Console.WriteLine($"[{DateTime.Now}] 按钮点击事件已记录。发送者:{sender}");
}
}
// 使用示例
class Program
{
static void Main()
{
Button btn = new Button();
Logger logger = new Logger();
// 订阅事件:使用 += 运算符
btn.Clicked += logger.LogButtonClick;
// 触发事件
btn.SimulateClick();
// 输出:
// 按钮被点击了!
// [2023-10-27 10:30:00] 按钮点击事件已记录。发送者:Button
}
}
3.2 自定义事件参数(EventArgs)
如果需要传递更多数据,可以创建自定义事件参数类,继承自 EventArgs。
csharp
// 自定义事件参数
public class TemperatureChangedEventArgs : EventArgs
{
public double OldTemperature { get; }
public double NewTemperature { get; }
public TemperatureChangedEventArgs(double oldTemp, double newTemp)
{
OldTemperature = oldTemp;
NewTemperature = newTemp;
}
}
// 温度传感器(发布者)
public class TemperatureSensor
{
private double _currentTemp;
// 使用泛型 EventHandler<TEventArgs>
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
public double CurrentTemperature
{
get => _currentTemp;
set
{
if (Math.Abs(_currentTemp - value) > 0.1) // 温度变化超过0.1度时触发
{
var oldTemp = _currentTemp;
_currentTemp = value;
OnTemperatureChanged(oldTemp, value);
}
}
}
protected virtual void OnTemperatureChanged(double oldTemp, double newTemp)
{
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
}
}
// 订阅者:温度显示器
public class TemperatureDisplay
{
public void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"温度变化:{e.OldTemperature:F1}°C → {e.NewTemperature:F1}°C");
}
}
3.3 自定义委托类型的事件(传统方式)
在 C# 1.0 时代常见,现在推荐使用 EventHandler,但为了兼容旧代码仍需了解。
csharp
// 自定义委托类型
public delegate void MyCustomEventHandler(object sender, string message);
public class Publisher
{
// 声明基于自定义委托的事件
public event MyCustomEventHandler CustomEvent;
public void DoSomething()
{
CustomEvent?.Invoke(this, "自定义事件被触发!");
}
}
4. 事件访问器(add/remove)
默认情况下,事件的 += 和 -= 操作由编译器自动生成。但你可以像属性一样,提供自定义的添加/移除逻辑。
csharp
public class EventWithAccessor
{
private EventHandler _myEvent;
public event EventHandler MyEvent
{
add
{
Console.WriteLine($"添加事件处理程序:{value.Method.Name}");
_myEvent += value;
}
remove
{
Console.WriteLine($"移除事件处理程序:{value.Method.Name}");
_myEvent -= value;
}
}
public void RaiseEvent()
{
_myEvent?.Invoke(this, EventArgs.Empty);
}
}
5. 事件使用的最佳实践
- 命名约定 :事件名使用动词或动词短语(如
Clicked、DataReceived)。触发事件的方法通常命名为OnEventName。 - 线程安全:在多线程环境中,应先将事件引用复制到局部变量再触发,避免竞态条件。
- 空条件运算符 :始终使用
?.Invoke()检查事件是否为 null。 - EventArgs.Empty :当不需要传递额外数据时,使用
EventArgs.Empty而不是new EventArgs()。 - 及时取消订阅 :避免内存泄漏,当订阅者不再需要接收事件时,使用
-=取消订阅。
6. 事件 vs 委托 vs 回调
| 特性 | 事件 | 委托 | 回调(如 Action/Func) |
|---|---|---|---|
| 封装性 | 高(外部只能 +=/-=) | 中(可赋值、调用) | 低(完全公开) |
| 多播 | 支持 | 支持 | 单播(但可通过委托实现) |
| 典型用途 | 发布-订阅模式 | 方法指针、回调基础 | 简单回调、Lambda 表达式 |
| 安全性 | 外部不能直接触发 | 外部可随意调用 | 外部可随意调用 |
7. 实战示例:简单的订单系统
csharp
// 订单事件参数
public class OrderPlacedEventArgs : EventArgs
{
public string OrderId { get; }
public decimal TotalAmount { get; }
public DateTime OrderTime { get; }
public OrderPlacedEventArgs(string orderId, decimal totalAmount)
{
OrderId = orderId;
TotalAmount = totalAmount;
OrderTime = DateTime.Now;
}
}
// 订单服务(发布者)
public class OrderService
{
public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
public void PlaceOrder(string orderId, decimal amount)
{
// 业务逻辑...
Console.WriteLine($"订单 {orderId} 已创建,金额:{amount:C}");
// 触发事件
OnOrderPlaced(new OrderPlacedEventArgs(orderId, amount));
}
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e)
{
OrderPlaced?.Invoke(this, e);
}
}
// 订阅者1:库存服务
public class InventoryService
{
public void UpdateInventory(object sender, OrderPlacedEventArgs e)
{
Console.WriteLine($"[库存] 订单 {e.OrderId} 已处理,开始扣减库存...");
}
}
// 订阅者2:邮件服务
public class EmailService
{
public void SendConfirmationEmail(object sender, OrderPlacedEventArgs e)
{
Console.WriteLine($"[邮件] 已向客户发送订单 {e.OrderId} 的确认邮件");
}
}
// 使用
class Program
{
static void Main()
{
OrderService orderService = new OrderService();
InventoryService inventory = new InventoryService();
EmailService email = new EmailService();
// 订阅事件
orderService.OrderPlaced += inventory.UpdateInventory;
orderService.OrderPlaced += email.SendConfirmationEmail;
// 下订单
orderService.PlaceOrder("ORD-20231027-001", 299.99m);
// 输出:
// 订单 ORD-20231027-001 已创建,金额:¥299.99
// [库存] 订单 ORD-20231027-001 已处理,开始扣减库存...
// [邮件] 已向客户发送订单 ORD-20231027-001 的确认邮件
}
}
8. 常见问题与注意事项
8.1 内存泄漏问题
事件订阅会保持对订阅者对象的引用,如果订阅者是短生命周期对象而发布者是长生命周期对象,可能导致订阅者无法被垃圾回收。
解决方案:在适当的时候取消订阅(如 Dispose 方法中)。
8.2 异常处理
事件处理程序中抛出异常会中断后续处理程序的执行。建议在每个处理程序内部处理异常。
csharp
// 不安全的做法
public event EventHandler MyEvent;
// 安全的做法:封装调用
protected virtual void OnMyEvent()
{
var handlers = MyEvent?.GetInvocationList();
if (handlers != null)
{
foreach (EventHandler handler in handlers)
{
try
{
handler.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
// 记录异常,但不中断其他处理程序
Console.WriteLine($"事件处理程序异常:{ex.Message}");
}
}
}
}
8.3 事件与接口
事件可以作为接口成员声明,实现类必须提供该事件。
csharp
public interface IObservable
{
event EventHandler SomethingHappened;
}
public class MyObservable : IObservable
{
public event EventHandler SomethingHappened;
}
9. 总结
C# 事件是 .NET 事件驱动编程的核心机制,它:
- 提供了一种标准化的发布-订阅实现方式
- 通过委托实现类型安全的多播
- 支持自定义事件参数传递数据
- 遵循 .NET 框架设计指南,具有良好的互操作性
掌握事件的使用,能够让你编写出更加松耦合、可扩展的 C# 应用程序。在实际开发中,结合异步编程(async/await)和 Reactive Extensions(Rx.NET)等现代技术,可以构建出更加强大的事件驱动系统。