事件是编译器强制的访问约束机制,禁止外部直接赋值、调用invoke或读取委托链;仅允许+=和-=操作,确保通知安全性与封装性。

事件不是“带访问修饰的委托”,它是编译器强制施加的一套访问约束机制。直接把 public Action<string> OnChanged;</string> 换成 public event Action<string> Changed;</string>,外部代码就再也不能用 = 赋值、不能调用 Invoke、也不能读取其内部委托链——这些都不是约定,而是编译期报错。
为什么 public delegate 字段会导致封装失控
公开委托字段会让调用方拥有完全控制权:既能替换整个委托链,也能伪造触发,还能绕过业务逻辑直接“喊话”。这在模块边界清晰的系统里尤其危险。
-
OnDataReady被外部设为null,导致后续所有通知静默丢失,且无任何编译警告 - 客户端代码直接调用
OnDataReady?.Invoke(...),跳过类内状态校验(比如是否已完成初始化) - 多个模块反复用
=覆盖彼此的订阅,最后只剩最后一个注册者生效
event 关键字到底禁止了什么操作
event 不是语法糖,它在 IL 层面生成了独立的 add_XXX 和 remove_XXX 方法,并把字段本身设为私有。这意味着:
- 外部代码只能用
+=和-=—— 编译器会转成对add_XXX的调用 -
myObj.Changed = handler;会报 CS0070:“事件只能出现在 += 或 -= 的左侧” -
myObj.Changed?.Invoke(...)会报 CS0071:“无法通过实例引用访问事件” - 即使反射也拿不到底层委托字段(.NET 5+ 默认隐藏
e__BackingField)
什么时候该用委托而不是事件
事件只解决“通知”问题;委托更适合“行为注入”或“策略传递”。别为了统一风格硬套 event。
- 需要把方法当参数传给另一个方法时,用
Action或Func—— 比如List<t>.ForEach(Action<t>)</t></t> - 实现策略模式:类内部根据条件选择执行哪个委托,如
private readonly Func<int bool> _validator;</int> - 跨层回调且调用方需控制执行时机(比如异步完成后的清理动作),此时委托比事件更直白
- 测试替换成 Mock 委托时,
event需要额外包装一层,而委托字段可直接赋值替换
容易被忽略的线程安全细节
多播委托本身不是线程安全的:+= 和 -= 是读-改-写操作。事件虽限制了赋值,但没解决并发订阅/取消的问题。
-
EventHandler<t></t>的默认实现不保证线程安全;高并发场景下可能抛出NullReferenceException(因内部委托被其他线程置空) - 正确做法是在触发前做原子快照:
var handler = Changed;再判空调用,而非直接Changed?.Invoke(...) - 若需强一致性,应配合
lock或使用ConcurrentDictionary管理订阅者,而非依赖event自身










