事件处理程序和协方差

本文关键字:方差 程序 事件处理 | 更新日期: 2023-09-27 18:29:33

我一直在尝试创建一个通用事件。基本上它应该看起来像这样:

namespace DelegateTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var lol = new SomeClass();
            lol.SomeEvent += handler;
        }
        static void handler(object sender, SomeDerivedClass e)
        {
        }
    }
    class SomeClass
    {
        public delegate void SomeEventDelegate<in T>(object sender, T data);
        public event SomeEventDelegate<ISomeInterface> SomeEvent;
    }
    interface ISomeInterface
    {
    }
    class SomeDerivedClass : ISomeInterface
    {
    }
}

我想允许用户传递任何委托,它的第二个参数派生自"ISomeInterface"。

"in"指定逆方差,对吧?这意味着如果 API 期望更通用的东西,你可以传递给它更具体的东西(在我的基础中,"ISomeInterface"是通用的,而我的"SomeDerivedClass"将是特定的。但是,我被告知我的编译器"方法处理程序没有与DelegateTest.SomeClass.SomeEventDelegate匹配的重载。

我想知道为什么这不起作用。如果是这样,会导致什么问题?还是我错过了一些东西才能让它工作?

提前感谢!

事件处理程序和协方差

"in"指定逆方差,对吧?

是的。

这意味着如果 API 期望更通用的东西,你可以传递给它更具体的东西(在我的基础中,"ISomeInterface"是通用的,而我的"SomeDerivedClass"是特定的(。

不。委托逆变允许委托引用参数类型比委托类型派生更少的方法。例如,假设ISomeInterface有一个基本接口:

interface ISomeBaseInterface
{
}
interface ISomeInterface : ISomeBaseInterface
{
}

假设handler拿了ISomeBaseInterface而不是SomeDerivedClass

static void handler(object sender, ISomeBaseInterface e) 

然后new SomeClass().SomeEvent += handler就会起作用。

这就是原始代码不是类型安全的原因:当SomeClass引发SomeEvent时,它可能会传递任何实现ISomeInterface作为data参数的内容。例如,它可以传递 SomeDerivedClass 的实例,但它也可以传递

class SomeOtherDerivedClass : ISomeInterface
{
}

如果您能够向事件注册void handler(object sender, SomeDerivedClass e),则该处理程序最终将使用 SomeOtherDerivedClass 调用,这不起作用。

总之,您可以注册比事件类型更通用的事件处理程序,而不是更具体的事件处理程序。

更新:您评论说:

好吧,我实际上想遍历列表并检查类型。因此,如果要使用类型为SomeOtherDerivedObject的数据对象触发事件,则程序将循环访问订阅该事件的方法列表,直到找到与签名匹配的方法(对象,SomeOtherDerivedObject(。因此,事件本身仅用于存储,而不用于实际调用委托。

我不认为 C# 允许您声明适用于任意委托类型的event。下面介绍如何编写添加事件处理程序并调用它们的方法:

class SomeClass
{
    private Delegate handlers;
    public delegate void SomeEventDelegate<in T>(object sender, T data);
    public void AddSomeEventHandler<T>(SomeEventDelegate<T> handler)
    {
        this.handlers = Delegate.Combine(this.handlers, handler);
    }
    protected void OnSomeEvent<T>(T data)
    {
        if (this.handlers != null)
        {
            foreach (SomeEventDelegate<T> handler in
                this.handlers.GetInvocationList().OfType<SomeEventDelegate<T>>())
            {
                handler(this, data);
            }
        }
    }
}

委托逆变的一个主要烦恼是,虽然委托类型例如 Action<Fruit>可以传递给一个期待Action<Banana>的例程,尝试组合两个实际类型为Action<Fruit>Action<Banana>的委托将失败*即使两个委托都有"编译时"类型Action<Banana>。 为了解决这个问题,我建议使用如下方法:

    static T As<T>(this Delegate del) where T : class
    {
        if (del == null || del.GetType() == typeof(T)) return (T)(Object)del;
        Delegate[] invList = ((Delegate)(Object)del).GetInvocationList();
        for (int i = 0; i < invList.Length; i++)
            if (invList[i].GetType() != typeof(T))
                invList[i] = Delegate.CreateDelegate(typeof(T), invList[i].Target, invList[i].Method);
        return (T)(Object)Delegate.Combine(invList);
    }

给定委托和委托类型,此方法将检查传入委托的类型是否与指定类型精确匹配;如果没有,但原始委托中的方法具有指定类型的正确签名,则将创建一个具有必要特征的新委托。 请注意,如果在两个不同的场合传递此函数的委托,这些委托类型不正确,但彼此比较相等,则此方法返回的委托也将相互相等。 因此,如果一个人有一个应该接受类型Action<string>委托的事件,则可以使用上述方法将例如传入的Action<object>转换为"真实"Action<string>,然后再从事件中添加或删除它。

如果要从正确委托类型的字段中添加或减去传入的委托,则如果使用以下方法,则可以改进类型推断和智能感知行为:

    static void AppendTo<T>(this Delegate newDel, ref T baseDel) where T : class
    {
        newDel = (Delegate)(Object)newDel.As<T>();
        T oldBaseDel, newBaseDel;
        do
        {
            oldBaseDel = baseDel;
            newBaseDel = (T)(Object)Delegate.Combine((Delegate)(object)oldBaseDel, newDel);
        } while (System.Threading.Interlocked.CompareExchange(ref baseDel, newBaseDel, oldBaseDel) != oldBaseDel);
    }
    static void SubtractFrom<T>(this Delegate newDel, ref T baseDel) where T : class
    {
        newDel = (Delegate)(Object)newDel.As<T>();
        T oldBaseDel, newBaseDel;
        do
        {
            oldBaseDel = baseDel;
            newBaseDel = (T)(Object)Delegate.Remove((Delegate)(object)oldBaseDel, newDel);
        } while (System.Threading.Interlocked.CompareExchange(ref baseDel, newBaseDel, oldBaseDel) != oldBaseDel);
    }

这些方法将作为派生自Delegate类型的扩展方法出现,并允许在合适的委托类型的变量或字段中添加或减去此类类型的实例;这种加法或减法将以线程安全的方式完成,因此可以在事件添加/删除方法中使用这些方法,而无需额外的锁定。