C# 事件(Event)

`event` 关键字、发布订阅、`EventHandler`、以及取消订阅。

Intermediate
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
裸委托
AspectPublic delegate fieldevent
外部订阅能能
外部触发能(危险)不能
外部清空能(危险)不能
外部赋值能(覆盖)不能
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 ¥199

EventHandler<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 事件、领域事件、进度回调)
  • 该用 —— 发布者不该知道订阅者是谁
  • 不用 —— 一对一调用,直接传委托或调方法更清楚
  • 慎用 —— 事件链太长会让调用栈难追踪,考虑用消息总线

Friends & Partners