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