C#中的事件(event)如何使用 - 发布-订阅模式的经典实现

来源:这里教程网 时间:2026-02-21 17:32:34 作者:

C# 中的

event
是对发布-订阅(Publish-Subscribe)模式的原生支持,它封装了委托(delegate)的调用逻辑,确保外部代码只能“订阅”或“取消订阅”,不能直接触发或清空事件,从而保障了封装性和安全性。

事件的本质是受保护的委托字段

事件底层基于委托,但比普通委托更严格。声明一个事件时,编译器会自动生成一个私有委托字段,并为

+=
-=
提供线程安全的访问器(在 .NET Core/.NET 5+ 中默认使用
Interlocked.CompareExchange
保证原子性)。

例如:

public event EventHandler DataReceived;

等价于:一个私有

EventHandler<dataeventargs></dataeventargs>
字段 + 公开的
add
/
remove
访问器 —— 外部无法直接赋值(如
DataReceived = null
)或调用(如
DataReceived(...)
),必须通过类内部触发。

标准写法:定义事件、触发事件、订阅事件

典型三步走,遵循 .NET 命名与设计规范:

定义事件:使用
EventHandler<t></t>
或自定义委托,参数类型继承自
EventArgs
触发事件:先判空(或用 C# 6 的空条件调用
?.Invoke()
),再调用
订阅事件:用
+=
绑定方法(支持 Lambda、本地函数、实例/静态方法)

示例:

public class Sensor
{
  public event EventHandler DataUpdated;

  public void Read() {
    var data = new SensorData { Value = DateTime.Now.Second };
    DataUpdated?.Invoke(this, new SensorDataEventArgs(data));
  }
}

// 使用时:
var sensor = new Sensor();
sensor.DataUpdated += (sender, e) => Console.WriteLine($"新数据:{e.Data.Value}");
sensor.Read();

避免常见陷阱

几个高频出错点,直接影响健壮性:

不判空直接调用:事件没人订阅时为
null
,直接
Invoke()
NullReferenceException
在多线程中未同步触发:虽然
+=
/
-=
是线程安全的,但事件字段本身可能被并发修改;若需绝对安全,可用
lock
Interlocked
包裹触发逻辑(尤其在旧框架中)
忘记取消订阅导致内存泄漏:尤其在长生命周期对象(如窗体、服务)订阅短生命周期对象事件时,应显式用
-=
解绑,或使用弱事件模式
用字段代替事件暴露委托:如
public EventHandler MyHandler;
—— 这破坏封装,外部可随意赋值或调用,不是事件

进阶:自定义事件参数与泛型事件

推荐继承

EventArgs
封装业务数据,语义清晰且符合约定:

public class OrderPlacedEventArgs : EventArgs
{
  public Order Order { get; }
  public DateTime Timestamp { get; }

  public OrderPlacedEventArgs(Order order) : base()
  {
    Order = order;
    Timestamp = DateTime.UtcNow;
  }
}

然后声明:
public event EventHandler OrderPlaced;
这样调用方能明确知道事件携带什么信息,IDE 也能更好推导类型。

如果不想依赖

EventArgs
,也可用泛型委托如
Action<t></t>
,但会失去事件的标准语义和工具链支持(如设计器、WPF 路由事件),一般不推荐用于公开 API。

基本上就这些。事件不是语法糖,而是 C# 对松耦合通信的基础设施级支持 —— 写清楚谁发布、谁响应、数据怎么传,剩下的交给语言和运行时。

相关推荐

热文推荐