在软件世界里,对象之间的沟通是个永恒的难题。你怎么让一个订单服务在完成下单后,同时通知邮件系统、库存系统,甚至日志系统------却又不让它们彼此紧紧绑死?C# 的 event 机制,就是为这个问题量身打造的答案。它的底层逻辑是发布-订阅模式:一个对象喊一声,所有关心这件事的人都能听到,而喊话的人根本不需要知道谁在听。
🧱 从委托说起,事件的根在这里
要理解 event,绕不开委托(Delegate) 。委托是 C# 里一种类型安全的"函数指针"------它描述的不是数据,而是一种方法的形状:接受什么参数、返回什么类型。
csharp
// 定义一个委托类型:接受一个 string,无返回值
public delegate void NotifyHandler(string message);
有了委托,你就能把方法像变量一样传来传去。但裸委托有个危险:外部代码可以直接调用它,甚至用 = 一刀把所有订阅者清空。event 关键字的存在,就是给委托套上一层保护壳------外部只能 += 订阅或 -= 退订,触发的权力永远留在发布者手里。
csharp
public event NotifyHandler OnNotify;
// 外部能做的只有:OnNotify += xxx 或 OnNotify -= xxx
// 想直接 OnNotify(...) ?对不起,编译器不答应
📐 .NET 的标准姿势:EventHandler<T>
自己定义委托当然可以,但 .NET 早就备好了两个通用委托,覆盖了绝大多数场景:
| 委托类型 | 签名 | 适用场景 |
|---|---|---|
EventHandler |
(object sender, EventArgs e) |
事件不需要携带额外数据 |
EventHandler<T> |
(object sender, T e) |
事件需要携带自定义数据 |
其中 sender 是触发事件的对象本身,e 是事件数据。这个约定在整个 .NET 生态里高度统一,跟着走准没错。
当事件需要携带数据时,继承 EventArgs 写一个数据类:
csharp
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
🔍 完整示例:一个订单,三方响应
用电商下单场景把所有概念串起来。一笔订单成交后,邮件服务要发确认邮件,库存服务要锁库存------两件事同时发生,互不干扰。
csharp
using System;
// ── 事件数据类 ──────────────────────────────────
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(int orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}
}
// ── 发布者:订单服务 ────────────────────────────
public class OrderService
{
// 声明事件
public event EventHandler<OrderEventArgs>? OrderPlaced;
public event EventHandler<OrderEventArgs>? OrderCancelled;
// 用受保护的方法封装触发逻辑,方便子类重写
protected virtual void OnOrderPlaced(OrderEventArgs e)
{
// ?. 确保没有订阅者时不会崩
OrderPlaced?.Invoke(this, e);
}
protected virtual void OnOrderCancelled(OrderEventArgs e)
{
OrderCancelled?.Invoke(this, e);
}
public void PlaceOrder(int orderId, decimal amount)
{
Console.WriteLine($"[OrderService] 处理订单 #{orderId},金额:{amount:C}");
// 业务逻辑处理完毕,开口通知
OnOrderPlaced(new OrderEventArgs(orderId, amount));
}
public void CancelOrder(int orderId, decimal amount)
{
Console.WriteLine($"[OrderService] 取消订单 #{orderId}");
OnOrderCancelled(new OrderEventArgs(orderId, amount));
}
}
// ── 订阅者:邮件服务 ────────────────────────────
public class EmailService
{
public void OnOrderPlaced(object? sender, OrderEventArgs e)
{
Console.WriteLine($" [EmailService] 发送确认邮件:订单 #{e.OrderId},金额 {e.Amount:C}");
}
public void OnOrderCancelled(object? sender, OrderEventArgs e)
{
Console.WriteLine($" [EmailService] 发送取消通知:订单 #{e.OrderId}");
}
}
// ── 订阅者:库存服务 ────────────────────────────
public class InventoryService
{
public void OnOrderPlaced(object? sender, OrderEventArgs e)
{
Console.WriteLine($" [InventoryService] 锁定库存:订单 #{e.OrderId}");
}
public void OnOrderCancelled(object? sender, OrderEventArgs e)
{
Console.WriteLine($" [InventoryService] 释放库存:订单 #{e.OrderId}");
}
}
// ── 主程序 ──────────────────────────────────────
class Program
{
static void Main()
{
var orderService = new OrderService();
var emailService = new EmailService();
var inventoryService = new InventoryService();
// 订阅:告诉发布者"我关心这件事"
orderService.OrderPlaced += emailService.OnOrderPlaced;
orderService.OrderPlaced += inventoryService.OnOrderPlaced;
orderService.OrderCancelled += emailService.OnOrderCancelled;
orderService.OrderCancelled += inventoryService.OnOrderCancelled;
orderService.PlaceOrder(1001, 299.99m);
Console.WriteLine();
orderService.CancelOrder(1001, 299.99m);
Console.WriteLine("\n--- 邮件服务退订后 ---\n");
// 退订:从此这个事件跟我无关
orderService.OrderPlaced -= emailService.OnOrderPlaced;
orderService.PlaceOrder(1002, 599.00m);
}
}
运行结果清晰地展示了多播的威力------一次 PlaceOrder,两个服务同时响应:
csharp
[OrderService] 处理订单 #1001,金额:¥299.99
[EmailService] 发送确认邮件:订单 #1001,金额 ¥299.99
[InventoryService] 锁定库存:订单 #1001
[OrderService] 取消订单 #1001
[EmailService] 发送取消通知:订单 #1001
[InventoryService] 释放库存:订单 #1001
--- 邮件服务退订后 ---
[OrderService] 处理订单 #1002,金额:¥599.00
[InventoryService] 锁定库存:订单 #1002
⚡ Lambda 订阅:方便,但有代价
不想专门写一个方法?Lambda 直接订阅也行,代码更紧凑:
csharp
orderService.OrderPlaced += (sender, e) =>
{
Console.WriteLine($"Lambda 收到:订单 #{e.OrderId}");
};
但这里有个坑要记住:Lambda 订阅之后无法退订 。每次写 += 后面跟 Lambda,编译器都会生成一个新的匿名对象,你手里根本没有引用,自然也就没法 -=。需要退订的场景,老老实实用具名方法。
⚙️ event 与裸委托:一张表说清楚
两者看起来相似,本质上差了一层封装:
| 特性 | 裸委托 | event 事件 |
|---|---|---|
| 外部直接调用 | ✅ 允许 | ❌ 禁止 |
外部直接赋值 = |
✅ 允许(危险) | ❌ 禁止 |
| 多播支持 | ✅ | ✅ |
| 封装性 | 弱 | 强 |
| 推荐场景 | 回调、函数传递 | 对象间事件通知 |
简单说:委托是机制,事件是规范 。在对象通信的场景里,永远优先选 event。
🎯 几条用好事件的心得
写事件代码时,有几个习惯养成了会省很多麻烦。触发事件时始终用 ?.Invoke() ,一个问号号就能挡住空引用异常。把触发逻辑封装进 protected virtual OnXxx() 方法,子类想改行为直接重写,干净利落。
内存泄漏是事件最常见的隐患------只要订阅者还挂在发布者的事件上,GC 就不会回收它,哪怕你以为它早该消失了。用完记得 -=,这个习惯在长生命周期对象里尤其关键。
事件驱动的代码写顺了,你会发现对象之间的耦合悄悄松开了------每个模块只管自己的事,通过事件说话,整个系统反而比紧密绑定时更容易扩展和测试。这大概就是它在 .NET 里活了这么多年、依然是核心特性的原因。