C#中如何实现事件参数含泛型属性的自定义事件
你之前的实现无法编译的根源是C#泛型的编译期静态类型约束:FooPropertyChangingEventHandler<int>和FooPropertyChangingEventHandler<string>是完全独立的两个委托类型,和List<int>与List<string>不能互相赋值同理,你不可能用同一个事件字段同时承载不同类型参数的泛型委托,这和泛型运行时确定具体类型的特性不冲突——运行时每个泛型实例确实是明确类型,但编译期必须给事件、委托明确的类型签名,不然编译器没法做类型校验。
你的判断「事件订阅者无法预先获知事件参数的属性类型」只适用于你最初把所有属性变更塞到同一个泛型事件的写法,换个实现结构完全可以实现你要的效果,不需要把ExistingValue定义为object丢失类型安全。
这个方案完全匹配你的需求:单个事件适配所有类型属性的变更场景,保留ExistingValue的泛型强类型,事件处理程序不需要绑定固定泛型类型,没有多余的性能损耗。
第一步:定义非泛型事件参数基类
把所有不依赖泛型的公共属性(Cancel、PropertyName)放到抽象基类中,作为所有泛型事件参数的统一父类型:
public abstract class FooPropertyChangingEventArgs : EventArgs { protected FooPropertyChangingEventArgs(string propertyName) { PropertyName = propertyName; } public bool Cancel { get; set; } public string PropertyName { get; } // 提供非泛型的值访问入口,供不需要强类型的场景使用 public abstract object ExistingValueBoxed { get; } }
第二步:定义泛型事件参数子类
继承非泛型基类,实现泛型的强类型ExistingValue属性:
public class FooPropertyChangingEventArgs<T> : FooPropertyChangingEventArgs { public FooPropertyChangingEventArgs(string propertyName, T existingValue) : base(propertyName) { ExistingValue = existingValue; } public override object ExistingValueBoxed => ExistingValue; // 保留你需要的强类型值属性 public T ExistingValue { get; } }
第三步:定义事件与业务类
事件直接使用BCL自带的非泛型EventHandler<TEventArgs>委托,参数类型用非泛型基类,就可以兼容所有T的泛型事件参数实例:
public class Foo { // 单个事件适配所有属性变更,不需要指定泛型参数 public event EventHandler<FooPropertyChangingEventArgs> OnFooPropertyChanging; private int _myInt = 0; private string _myString = string.Empty; public Foo(int myInt, string myString) { _myInt = myInt; _myString = myString; } public int MyInt { get => _myInt; set { // 传入int类型的泛型参数实例 var args = new FooPropertyChangingEventArgs<int>("MyInt", _myInt); OnFooPropertyChanging?.Invoke(this, args); if(!args.Cancel) { _myInt = value; } } } public string MyString { get => _myString; set { // 传入string类型的泛型参数实例 var args = new FooPropertyChangingEventArgs<string>("MyString", _myString); OnFooPropertyChanging?.Invoke(this, args); if(!args.Cancel) { _myString = value; } } } }
第四步:事件订阅实现
不需要给事件处理方法指定固定泛型参数,需要访问强类型值时用C#的模式匹配直接转换即可,转换后拿到的ExistingValue就是对应强类型,无额外拆箱开销:
public class FooSubscriber { private Foo _foo; private int _runningTotal = 0; private string _runningString = string.Empty; public FooSubscriber() { _foo = new Foo(0, string.Empty); // 订阅事件完全和普通非泛型事件一致 _foo.OnFooPropertyChanging += _foo_PropertyChanging; } public void ChangeFoo(int newMyInt, string newMyString) { _foo.MyInt = newMyInt; _foo.MyString = newMyString; } private void _foo_PropertyChanging(object sender, FooPropertyChangingEventArgs e) { // 不需要访问值类型的通用逻辑,直接用基类属性即可 if(DateTime.Now.Month == 6) { e.Cancel = true; return; } switch(e) { // 匹配int类型的事件参数,直接拿到强类型值 case FooPropertyChangingEventArgs<int> intArgs when e.PropertyName == "MyInt": _runningTotal += intArgs.ExistingValue + 1; break; // 匹配string类型的事件参数,直接拿到强类型值 case FooPropertyChangingEventArgs<string> strArgs when e.PropertyName == "MyString": _runningString += strArgs.ExistingValue.ToUpper(); break; default: _runningTotal = 0; _runningString = string.Empty; break; } } }
如果你追求极致的编译期类型安全,不想在订阅端做任何类型判断,可以给每个属性单独定义对应类型的泛型事件:
public class Foo { // 每个属性对应自己类型的事件 public event EventHandler<FooPropertyChangingEventArgs<int>> OnMyIntChanging; public event EventHandler<FooPropertyChangingEventArgs<string>> OnMyStringChanging; private int _myInt = 0; private string _myString = string.Empty; // 构造函数逻辑不变 public int MyInt { get => _myInt; set { var args = new FooPropertyChangingEventArgs<int>("MyInt", _myInt); OnMyIntChanging?.Invoke(this, args); if(!args.Cancel) _myInt = value; } } public string MyString { get => _myString; set { var args = new FooPropertyChangingEventArgs<string>("MyString", _myString); OnMyStringChanging?.Invoke(this, args); if(!args.Cancel) _myString = value; } } }
这种方案下订阅事件时,处理方法的参数直接就是对应强类型的泛型事件参数,不需要任何转换,缺点是属性较多时会有一定重复代码。
内容的提问来源于stack exchange,提问作者GhettoKitty

