【问题标题】:EventHandlers and Covariance事件处理程序和协方差
【发布时间】:2012-03-31 14:11:47
【问题描述】:

我一直在尝试创建一个通用事件。基本上它应该是这样的:

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。”

我想知道为什么这不起作用。如果是的话会带来什么问题?还是我错过了什么让它工作?

提前致谢!

【问题讨论】:

标签: c# events delegates variance


【解决方案1】:

“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 的anything 作为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);
            }
        }
    }
}

【讨论】:

  • 谢谢。有没有办法让它反过来工作?我目前正在使用类似 Java 的方法(接口 + addListener。)
  • 如果您绝对确定 SomeClass 仅将 SomeDerivedClass 的实例作为 data 参数传递,那么您可以添加显式转换:lol.SomeEvent += (sender, e) =&gt; handler(sender, (SomeDerivedClass)e)。但是,您不妨将SomeEvent 声明为SomeEventDelegate&lt;SomeDerivedClass&gt;。
  • 好吧,我实际上想遍历列表并检查类型。因此,如果要使用类型为 SomeOtherDerivedObject 的数据对象触发事件,则程序将遍历订阅该事件的方法列表,直到找到与签名匹配的方法(对象,SomeOtherDerivedObject)。所以事件本身只用于存储,而不是实际调用代表。我希望这是可以理解的。
  • 谢谢,你的例子对我很有帮助!
【解决方案2】:

委托逆变的一个主要烦恼是,虽然委托类型为 e.g. Action&lt;Fruit&gt; 可能会被传递给期望Action&lt;Banana&gt; 的例程,尝试组合实际类型为Action&lt;Fruit&gt; 和Action&lt;Banana&gt; 的两个委托将失败*即使两个委托都具有“编译时”类型Action&lt;Banana&gt;。为了解决这个问题,我建议使用如下方法:

    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&lt;string&gt; 类型的委托,则可以使用上述方法进行转换,例如在从事件中添加或删除之前,将传入的 Action&lt;object&gt; 转换为“真实的”Action&lt;string&gt;。

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

    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 的类型的扩展方法出现,并允许将此类类型的实例添加到合适的委托类型的变量或字段中或从中减去;这种加法或减法将以线程安全的方式完成,因此可以在事件添加/删除方法中使用这些方法而无需额外锁定。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多