C# 事件(Event)
`event` 关键字、发布订阅、`EventHandler`、以及取消订阅。
Key Points事件是加了访问限制的委托:外部只能 `+=` / `-=`,不能直接调用或清空。
声明与订阅
C#
class Button
{
// 声明事件
public event Action<string>? Clicked;
public void Click(string name)
{
Console.WriteLine($"按钮 {name} 被点击");
Clicked?.Invoke(name); // 有订阅者才触发
}
}
var btn = new Button();
btn.Clicked += name => Console.WriteLine($"处理器 1:{name}");
btn.Clicked += name => Console.WriteLine($"处理器 2:{name}");
btn.Click("确定");Output
按钮 确定 被点击
处理器 1:确定
处理器 2:确定Clicked?.Invoke(...) is the standard form: with no subscribers Clicked is null and ?. guards it. Copying to a local first is the thread-safe variant.
event 与裸委托的区别
C#
class Publisher
{
public Action? RawDelegate; // 裸委托:外部能做任何事
public event Action? RealEvent; // 事件:外部只能 += / -=
}
var p = new Publisher();
p.RawDelegate += () => Console.WriteLine("裸委托");
p.RawDelegate(); // 外部能直接触发
p.RawDelegate = null; // 外部能清空所有订阅者
p.RealEvent += () => Console.WriteLine("事件");
// p.RealEvent(); // 编译错误:只能 += 或 -=
// p.RealEvent = null; // 编译错误:不能赋值Output
裸委托| Aspect | Public delegate field | event |
|---|---|---|
| 外部订阅 | 能 | 能 |
| 外部触发 | 能(危险) | 不能 |
| 外部清空 | 能(危险) | 不能 |
| 外部赋值 | 能(覆盖) | 不能 |
Using a bare delegate field instead of
event is dangerous: any code can obj.Callback = null and wipe everyone's subscriptions, or fire it themselves. event exists to close that hole.EventHandler 惯例
C#
// .NET 惯例:(sender, e) 两个参数
class OrderEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(string id, decimal amount)
{
OrderId = id;
Amount = amount;
}
}
class OrderService
{
public event EventHandler<OrderEventArgs>? OrderPaid;
public void Pay(string id, decimal amount)
{
OrderPaid?.Invoke(this, new OrderEventArgs(id, amount));
}
}
var svc = new OrderService();
svc.OrderPaid += (sender, e) =>
Console.WriteLine($"收到支付:{e.OrderId} ¥{e.Amount}");
svc.Pay("A001", 199m);Output
收到支付:A001 ¥199EventHandler<T> is (object? sender, T e) — the sender and the payload. Custom payloads derive from EventArgs. This convention makes every C# event look the same.
取消订阅与泄漏
C#
class Subscriber : IDisposable
{
private readonly OrderService _svc;
private readonly EventHandler<OrderEventArgs> _handler;
public Subscriber(OrderService svc)
{
_svc = svc;
_handler = (s, e) => Console.WriteLine("收到 " + e.OrderId);
_svc.OrderPaid += _handler;
}
public void Dispose()
{
_svc.OrderPaid -= _handler; // 必须能移除,所以要存变量
}
}
var svc = new OrderService();
using (var sub = new Subscriber(svc))
{
svc.Pay("A001", 100m);
}
// 出了 using 已取消订阅
svc.Pay("A002", 200m);
Console.WriteLine("结束");Output
收到 A001
结束Events hold strong references: while the publisher lives the subscriber cannot be collected. A long-lived publisher (singleton, static service) plus short-lived subscribers is the classic leak. Store the delegate and
-= in Dispose.什么时候用事件
- 该用 —— 一对多通知(UI 事件、领域事件、进度回调)
- 该用 —— 发布者不该知道订阅者是谁
- 不用 —— 一对一调用,直接传委托或调方法更清楚
- 慎用 —— 事件链太长会让调用栈难追踪,考虑用消息总线
声明与订阅
C#
class Button
{
// 声明事件
public event Action<string>? Clicked;
public void Click(string name)
{
Console.WriteLine($"按钮 {name} 被点击");
Clicked?.Invoke(name); // 有订阅者才触发
}
}
var btn = new Button();
btn.Clicked += name => Console.WriteLine($"处理器 1:{name}");
btn.Clicked += name => Console.WriteLine($"处理器 2:{name}");
btn.Click("确定");Output
按钮 确定 被点击
处理器 1:确定
处理器 2:确定Clicked?.Invoke(...) 是标准写法:没有订阅者时 Clicked 是 null,?. 避免空引用。也可以先复制到局部变量再判空,那是线程安全的写法。
event 与裸委托的区别
C#
class Publisher
{
public Action? RawDelegate; // 裸委托:外部能做任何事
public event Action? RealEvent; // 事件:外部只能 += / -=
}
var p = new Publisher();
p.RawDelegate += () => Console.WriteLine("裸委托");
p.RawDelegate(); // 外部能直接触发
p.RawDelegate = null; // 外部能清空所有订阅者
p.RealEvent += () => Console.WriteLine("事件");
// p.RealEvent(); // 编译错误:只能 += 或 -=
// p.RealEvent = null; // 编译错误:不能赋值Output
裸委托| 对比项 | 公共委托字段 | event |
|---|---|---|
| 外部订阅 | 能 | 能 |
| 外部触发 | 能(危险) | 不能 |
| 外部清空 | 能(危险) | 不能 |
| 外部赋值 | 能(覆盖) | 不能 |
用裸委托字段代替
event 是危险的:任何代码都能 obj.Callback = null 把别人的订阅全部干掉,或者自己触发它。event 就是为封这个口子而生的。EventHandler 惯例
C#
// .NET 惯例:(sender, e) 两个参数
class OrderEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Amount { get; }
public OrderEventArgs(string id, decimal amount)
{
OrderId = id;
Amount = amount;
}
}
class OrderService
{
public event EventHandler<OrderEventArgs>? OrderPaid;
public void Pay(string id, decimal amount)
{
OrderPaid?.Invoke(this, new OrderEventArgs(id, amount));
}
}
var svc = new OrderService();
svc.OrderPaid += (sender, e) =>
Console.WriteLine($"收到支付:{e.OrderId} ¥{e.Amount}");
svc.Pay("A001", 199m);Output
收到支付:A001 ¥199EventHandler<T> 的签名是 (object? sender, T e)。sender 是触发者,e 带数据。自定义事件类型要继承 EventArgs。这个惯例让所有 C# 事件用法一致。
取消订阅与泄漏
C#
class Subscriber : IDisposable
{
private readonly OrderService _svc;
private readonly EventHandler<OrderEventArgs> _handler;
public Subscriber(OrderService svc)
{
_svc = svc;
_handler = (s, e) => Console.WriteLine("收到 " + e.OrderId);
_svc.OrderPaid += _handler;
}
public void Dispose()
{
_svc.OrderPaid -= _handler; // 必须能移除,所以要存变量
}
}
var svc = new OrderService();
using (var sub = new Subscriber(svc))
{
svc.Pay("A001", 100m);
}
// 出了 using 已取消订阅
svc.Pay("A002", 200m);
Console.WriteLine("结束");Output
收到 A001
结束事件是强引用:发布者活着,订阅者就不会被 GC 回收。长生命周期的发布者(单例、静态服务)加上短生命周期的订阅者,就是典型的内存泄漏。订阅时存下委托,
Dispose 时 -=。什么时候用事件
- 该用 —— 一对多通知(UI 事件、领域事件、进度回调)
- 该用 —— 发布者不该知道订阅者是谁
- 不用 —— 一对一调用,直接传委托或调方法更清楚
- 慎用 —— 事件链太长会让调用栈难追踪,考虑用消息总线