事件与多播委托在IL层面有何区别
解读
国内Unity面试里,面试官抛出“IL层面”四个字,通常不是让你背IL指令表,而是验证两件事:
- 你是否真的用ILDasm/ILSpy看过编译产物;
- 你是否理解事件语法糖如何被C#编译器翻译成私有委托字段+add_/remove_访问器,从而在多播委托的基础上再套一层封装。
答不到“私有字段+访问器”这一层,基本会被判定为“只用过+=,没看过底层”。
知识点
- 多播委托(multicast delegate)
- 编译后就是class类型,继承自System.MulticastDelegate,内部维护_invocationList数组。
- 支持+=、-=,本质是Delegate.Combine/Remove静态方法。
- 事件(event)
- 只是C#语法标记;编译器在包含类型里生成:
a) 一个私有委托字段(名字与事件相同,但前缀<event>);
b) 一对public add_xxx/remove_xxx方法,标记为specialname;
c) 外部代码的+=/-=被重定向到这对方法,无法从外部直接赋值或调用Invoke,从而做到“订阅隔离”。
- 只是C#语法标记;编译器在包含类型里生成:
- IL差异速记
- 多播委托:ldfld直接拿到字段,callvirt Invoke。
- 事件:ldarg.0 → call add_xxx/remove_xxx,字段对外不可见;Invoke只能在类内部call。
答案
打开ILDasm可以看到:
- 声明一个public多播委托
public Action MyDelegate;
IL只生成字段.field public class [mscorlib]System.Action MyDelegate,外部可ldfld直接访问并invoke。 - 声明一个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细节,也体现代码隔离意识,容易加分。