C# 事件处理方法 C#如何实现事件的发布和订阅

3次阅读

c#事件必须用Event关键字修饰委托字段,否则缺乏封装性和线程安全;正确写法为public event action datareceived;,调用前需判空并快照委托变量,推荐使用eventhandler及自定义eventargs继承类。

C# 事件处理方法 C#如何实现事件的发布和订阅

事件声明必须用 event 关键字修饰

直接声明委托字段(如 public Action<String> OnDataReceived;</string>)不是事件,它不具备封装性和线程安全防护。C# 事件要求用 event 修饰符包装委托类型,编译器会自动生成 addremove 访问器,限制外部代码只能用 +=-= 操作,防止被意外赋值或清空。

常见错误是漏写 event,导致订阅者能直接覆写整个委托链:publisher.OnDataReceived = NULL; —— 这会让所有已注册的处理方法丢失。

  • 正确写法:public event Action<string> DataReceived;</string>
  • 推荐使用泛型 EventHandler<teventargs></teventargs> 而非裸 Action,便于后期扩展事件参数
  • 自定义事件参数需继承 EventArgs,哪怕为空:例如 public class FileProcessedEventArgs : EventArgs { public string FileName { get; } }

Invoke 前必须判空,且推荐用局部变量缓存

调用事件前若不检查是否为 null,会在无订阅者时抛出 NullReferenceException。但直接写 if (DataReceived != null) DataReceived("hello"); 存在线程安全风险:判断后、调用前,最后一个订阅者可能已被移除。

标准做法是将事件快照到局部委托变量再调用:

var handler = DataReceived; if (handler != null) {     handler("hello"); }

这个模式在 .NET Core 2.1+ 中仍是推荐实践;C# 6 引入的空条件运算符 DataReceived?.Invoke("hello"); 看似简洁,但底层仍会做一次空检查 + 一次调用,无法完全规避竞态——不过对大多数 ui 或非高并发场景已足够安全。

订阅时注意方法签名匹配与生命周期管理

事件处理方法的参数和返回值必须严格匹配事件委托定义。例如事件声明为 event EventHandler<fileprocessedeventargs> FileProcessed;</fileprocessedeventargs>,那么订阅方法必须是 void OnFileProcessed(Object sender, FileProcessedEventArgs e) 形式。

  • 匿名方法Lambda 表达式可简化订阅:publisher.FileProcessed += (s, e) => console.WriteLine(e.FileName);
  • 务必在对象销毁前取消订阅(尤其在长生命周期对象监听短生命周期对象时),否则引发内存泄漏 —— 例如窗体订阅了后台服务事件,但没在 DisposeFormClosed-=
  • 避免重复订阅同一方法:多次 += 会导致触发多次;若需幂等,应自行维护订阅状态或改用弱事件模式

自定义事件发布逻辑要控制调用时机和上下文

事件不是自动“发布”的,它只是委托调用的语法糖。真正决定何时触发、以什么线程/上下文触发,由你控制。例如:

  • UI 控件事件(如 Button.Click)默认在 UI 线程同步触发;而异步任务完成后的事件(如 HttpClient 的响应处理)常需手动切换回 UI 线程,否则更新控件会抛 InvalidOperationException
  • 某些场景需要异步触发事件(避免阻塞发布者):Task.Run(() => handler?.Invoke(this, e));,但要注意这会丢失调用和异常传播路径
  • 如果事件逻辑较重,考虑引入事件总线(如 MediatR)解耦,而非在领域对象中直接 Invoke

最易被忽略的是:事件发布不等于业务完成。比如一个 Saved 事件在文件写入成功后触发,但如果后续日志记录失败,整个操作实际未原子完成——事件只是通知机制,不承担事务语义。

text=ZqhQzanResources