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

事件声明必须用 event 关键字修饰
直接声明委托字段(如 public Action<string> OnDataReceived;</string>)不是事件,它不具备封装性和线程安全防护。C# 事件要求用 event 修饰符包装委托类型,编译器会自动生成 add 和 remove 访问器,限制外部代码只能用 += 或 -= 操作,防止被意外赋值或清空。
常见错误是漏写 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); - 务必在对象销毁前取消订阅(尤其在长生命周期对象监听短生命周期对象时),否则引发内存泄漏 —— 例如窗体订阅了后台服务事件,但没在
Dispose或FormClosed中-= - 避免重复订阅同一方法:多次
+=会导致触发多次;若需幂等,应自行维护订阅状态或改用弱事件模式
自定义事件发布逻辑要控制调用时机和上下文
事件不是自动“发布”的,它只是委托调用的语法糖。真正决定何时触发、以什么线程/上下文触发,由你控制。例如:
- UI 控件事件(如
Button.Click)默认在 UI 线程同步触发;而异步任务完成后的事件(如HttpClient的响应处理)常需手动切换回 UI 线程,否则更新控件会抛InvalidOperationException - 某些场景需要异步触发事件(避免阻塞发布者):
Task.Run(() => handler?.Invoke(this, e));,但要注意这会丢失调用栈和异常传播路径 - 如果事件逻辑较重,考虑引入事件总线(如
MediatR)解耦,而非在领域对象中直接Invoke
最易被忽略的是:事件发布不等于业务完成。比如一个 Saved 事件在文件写入成功后触发,但如果后续日志记录失败,整个操作实际未原子完成——事件只是通知机制,不承担事务语义。










