事件与多播委托在IL层面有何区别

解读

国内Unity面试里,面试官抛出“IL层面”四个字,通常不是让你背IL指令表,而是验证两件事:

  1. 你是否真的用ILDasm/ILSpy看过编译产物;
  2. 你是否理解事件语法糖如何被C#编译器翻译成私有委托字段+add_/remove_访问器,从而在多播委托的基础上再套一层封装。
    答不到“私有字段+访问器”这一层,基本会被判定为“只用过+=,没看过底层”。

知识点

  1. 多播委托(multicast delegate)
    • 编译后就是class类型,继承自System.MulticastDelegate,内部维护_invocationList数组。
    • 支持+=、-=,本质是Delegate.Combine/Remove静态方法。
  2. 事件(event)
    • 只是C#语法标记;编译器在包含类型里生成:
      a) 一个私有委托字段(名字与事件相同,但前缀<event>);
      b) 一对public add_xxx/remove_xxx方法,标记为specialname;
      c) 外部代码的+=/-=被重定向到这对方法,无法从外部直接赋值或调用Invoke,从而做到“订阅隔离”。
  3. IL差异速记
    • 多播委托:ldfld直接拿到字段,callvirt Invoke。
    • 事件:ldarg.0 → call add_xxx/remove_xxx,字段对外不可见;Invoke只能在类内部call。

答案

打开ILDasm可以看到:

  1. 声明一个public多播委托public Action MyDelegate;
    IL只生成字段.field public class [mscorlib]System.Action MyDelegate,外部可ldfld直接访问并invoke。
  2. 声明一个public事件public event Action MyEvent;
    编译器额外生成:
    • .field private class [mscorlib]System.Action '<MyEvent>k__BackingField'
    • .method public specialname instance void add_MyEvent(class [mscorlib]System.Action value)
    • .method public specialname instance void remove_MyEvent(class [mscorlib]System.Action value)
      外部IL代码写obj.MyEvent += Handler;时,编译器自动翻译成callvirt instance void add_MyEvent无法直接触碰底层委托字段,也就无法Invoke或清空,这就是二者在IL层面的核心区别:事件在委托之上加了封装与方法级访问屏障

拓展思考

Unity热更场景(xlua/ILRuntime)经常利用这一差异做“安全事件”:

  • 在基类里把真实委托字段设成private,只暴露add/remove,防止热更脚本意外清空主工程事件。
  • 反射取事件时,若用GetField会返回null,必须用GetAddMethod()才能挂接,面试时可顺带提到,既展示IL细节,也体现代码隔离意识,容易加分。