C# 的 event 是对发布-订阅模式的原生支持,本质是受保护的委托字段,编译器自动生成私有委托及线程安全的 add/remove 访问器,确保外部只能订阅/取消订阅,不可直接触发或赋值,保障封装性与安全性。

C# 中的 event 是对发布-订阅(Publish-Subscribe)模式的原生支持,它封装了委托(delegate)的调用逻辑,确保外部代码只能“订阅”或“取消订阅”,不能直接触发或清空事件,从而保障了封装性和安全性。
事件的本质是受保护的委托字段
事件底层基于委托,但比普通委托更严格。声明一个事件时,编译器会自动生成一个私有委托字段,并为 += 和 -= 提供线程安全的访问器(在 .NET Core/.NET 5+ 中默认使用 Interlocked.CompareExchange 保证原子性)。
例如:
public event EventHandler等价于:一个私有 EventHandler 字段 + 公开的 add/remove 访问器 —— 外部无法直接赋值(如 DataReceived = null)或调用(如 DataReceived(...)),必须通过类内部触发。
标准写法:定义事件、触发事件、订阅事件
典型三步走,遵循 .NET 命名与设计规范:
-
定义事件:使用
EventHandler或自定义委托,参数类型继承自EventArgs -
触发事件:先判空(或用 C# 6 的空条件调用
?.Invoke()),再调用 -
订阅事件:用
+=绑定方法(支持 Lambda、本地函数、实例/静态方法)
示例:
public class Sensor{
public event EventHandler
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 Order Order { get; }
public DateTime Timestamp { get; }
public OrderPlacedEventArgs(Order order) : base()
{
Order = order;
Timestamp = DateTime.UtcNow;
}
}
然后声明:
public event EventHandler
这样调用方能明确知道事件携带什么信息,IDE 也能更好推导类型。
如果不想依赖 EventArgs,也可用泛型委托如 Action,但会失去事件的标准语义和工具链支持(如设计器、WPF 路由事件),一般不推荐用于公开 API。
基本上就这些。事件不是语法糖,而是 C# 对松耦合通信的基础设施级支持 —— 写清楚谁发布、谁响应、数据怎么传,剩下的交给语言和运行时。










