EventHandlers and Covariance

2019-05-02 09:19发布

I've been trying to create a generic event. Basically it should look like this:

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
    {
    }
}

I want to allow the user to pass any delegate which's second parameter is derived from "ISomeInterface."

"in" specifies contra-variance, right? That means if the API is expecting something more general, you can pass it something more specific (in my base "ISomeInterface" would be general, and my "SomeDerivedClass" would be specific.) I am, however, being told my the compiler that "no overload for method handler matches DelegateTest.SomeClass.SomeEventDelegate."

I am wondering why this isn't working. What are the problems that would be caused if it was? Or am I missing something for it to work?

Thanks in advance!

2条回答
【Aperson】
2楼-- · 2019-05-02 09:55

One major annoyance with delegate contravariance is that while a delegate of type e.g. Action<Fruit> may be passed to a routine expecting an Action<Banana>, an attempt to combine two delegates whose actual types are Action<Fruit> and Action<Banana> will fail *even if both delegates have "compile-time" type Action<Banana>. To get around this, I would suggest using a method like the following:

    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);
    }

Given a delegate and a delegate type, this method will check whether the type of the passed-in delegate precisely matches the specified type; if it doesn't, but the method(s) in the original delegate have the proper signatures for the specified type, a new delegate will be created with the necessary characteristics. Note that if on two separate occasions this function is passed delegates which are not of the proper type but which compare equal to each other, the delegates returned by this method will also compare equal to each other. Thus, if one has an event which is supposed to accept a delegate of type Action<string>, one could use the above method to convert e.g. a passed-in Action<object> into a "real" Action<string> before adding or removing it from the event.

If one will be adding or subtracting a passed-in delegate from a field of the proper delegate type, type inference and Intellisense behavior may be improved if one uses the following methods:

    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);
    }

These methods will appear as extension methods on types derived from Delegate, and will allow instances of such types to be added to or subtracted from variables or fields of suitable delegate types; such addition or subtraction will be done in thread-safe fashion, so it may be possible to use these methods in event add/remove methods without additional locking.

查看更多
Summer. ? 凉城
3楼-- · 2019-05-02 10:03

"in" specifies contra-variance, right?

Yes.

That means if the API is expecting something more general, you can pass it something more specific (in my base "ISomeInterface" would be general, and my "SomeDerivedClass" would be specific).

No. Delegate contravariance allows a delegate to reference a method with parameter types that are less derived than in the delegate type. For example, suppose ISomeInterface had a base interface:

interface ISomeBaseInterface
{
}

interface ISomeInterface : ISomeBaseInterface
{
}

And suppose handler took ISomeBaseInterface instead of SomeDerivedClass:

static void handler(object sender, ISomeBaseInterface e) 

Then new SomeClass().SomeEvent += handler would work.

Here's why the original code isn't type safe: When SomeClass raises SomeEvent, it can potentially pass anything that implements ISomeInterface as the data argument. For example, it could pass an instance of SomeDerivedClass, but it could also pass an instance of

class SomeOtherDerivedClass : ISomeInterface
{
}

If you were able to register void handler(object sender, SomeDerivedClass e) with the event, that handler would wind up being invoked with SomeOtherDerivedClass, which doesn't work.

In summary, you can register event handlers that are more general than the event type, not event handlers that are more specific.

UPDATE: You commented:

Well, I actually want to iterate through the list and check the types. So if the event was to be fired with a data object of type let's say SomeOtherDerivedObject, then the program would iterate through the list of methods that are subscribed to the event until it finds one that matches the signature (object, SomeOtherDerivedObject). So the event itself would only be used to store, not to actually call the delegates.

I don't think C# lets you declare an event that works with arbitrary delegate types. Here's how you can write methods that add event handlers and invoke them:

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);
            }
        }
    }
}
查看更多
登录 后发表回答