C#中使用逆变转换System.Action时出现错误的问题排查
我定义了接口IShootAttackChild,其中包含System.Action<IShootAttackChild>类型的事件OnDestroy;同时有一个BulletController类,自身带有System.Action<BulletController>类型的同名事件OnDestroy。让BulletController实现接口时,我尝试直接复用类内已有的事件,代码在Unity 2022.3.4f1中编译正常,但运行时执行OnDestroy += value;或OnDestroy -= value;会抛出ArgumentException:委托类型不兼容。原本以为基于委托的逆变特性可以直接转换,而使用OnDestroy += value.Invoke;的写法却能正常运行。
相关代码示例
接口定义
public interface IShootAttackChild { public event System.Action<IShootAttackChild> OnDestroy; /* 其他方法 */ }
初始类定义
public class BulletController { public event System.Action<BulletController> OnDestroy; }
编译正常但运行报错的实现代码
public class BulletController : IShootAttackChild { public event System.Action<BulletController> OnDestroy; event System.Action<IShootAttackChild> IShootAttackChild.OnDestroy { add { OnDestroy += value; } remove { OnDestroy -= value; } } }
可行的实现代码
public class BulletController : IShootAttackChild { public event System.Action<BulletController> OnDestroy; event System.Action<IShootAttackChild> IShootAttackChild.OnDestroy { add { OnDestroy += value.Invoke; } remove { OnDestroy -= value.Invoke; } } }
原因分析
逆变特性的适用范围误解:
Action<T>的泛型参数是in T(逆变),允许将接受基类型参数的委托赋值给接受派生类型参数的委托变量(例如Action<IShootAttackChild>可以赋值给Action<BulletController>,因为BulletController实现了IShootAttackChild)。但这种转换仅适用于单个委托的赋值,不适用于多播委托的直接合并。多播委托的类型严格匹配要求:
事件本质是多播委托,CLR在合并或移除多播委托时,要求委托的类型必须完全一致。虽然编译时C#编译器允许逆变的隐式转换,但运行时CLR会检查委托的实际类型,发现Action<IShootAttackChild>与Action<BulletController>类型不匹配,因此抛出ArgumentException。
解决办法
通过创建一个包装委托,将Action<BulletController>的调用转发给Action<IShootAttackChild>的委托:
- 使用
value.Invoke作为包装逻辑,当类的OnDestroy事件触发时,会将BulletController实例传入这个包装委托,再调用value.Invoke(IShootAttackChild)(因为BulletController是IShootAttackChild的实现,类型兼容)。 - 这种方式生成的包装委托类型是
Action<BulletController>,与类的事件类型完全一致,满足CLR的多播委托合并要求,因此可以正常运行。
内容的提问来源于stack exchange,提问作者Szymek

