You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 10:33:18