Co- and Contravariance bugs in .NET 4.0

2019-01-17 00:29发布

问题:

Some strange behavior with the C# 4.0 co- and contravariance support:

using System;

class Program {
  static void Foo(object x) { }
  static void Main() {
    Action<string> action = _ => { };

    // C# 3.5 supports static co- and contravariant method groups
    // conversions to delegates types, so this is perfectly legal:
    action += Foo;

    // since C# 4.0 much better supports co- and contravariance
    // for interfaces and delegates, this is should be legal too:
    action += new Action<object>(Foo);
  }
}

It's results with ArgumentException: Delegates must be of the same type.

Strange, isn't it? Why Delegate.Combine() (which is been called when performing += operation on the delegates) does not support co- and contravariance at runtime?

Moreover, I've found that BCL's System.EventHandler<TEventArgs> delegate type does not has contravariant annotation on it's generic TEventArgs parameter! Why? It's perfectly legal, TEventArgs type used only at input position. Maybe there is no contravariant annotation because of it nicely hides the bug with the Delegate.Combine()? ;)

p.s. All this affects the VS2010 RC and later versions.

回答1:

Long story short: delegate combining is all messed up with respect to variance. We discovered this late in the cycle. We're working with the CLR team to see if we can come up with some way to make all the common scenarios work without breaking backwards compatibility, and so on, but whatever we come up with will probably not make it into the 4.0 release. Hopefully we'll get it all sorted out in some service pack. I apologize for the inconvenience.



回答2:

Covariance and contravariance specifies inheritance relation between generic types. When you have covariance & contravariance, the classes G<A> and G<B> may be in some inheritance relationship depending on what A and B is. You can benefit from this when calling generic methods.

However, the Delegate.Combine method is not generic and the documentation clearly says when the exception will be thrown:

ArgumentException- Both a and b are not null reference (Nothing in Visual Basic), and a and b are not instances of the same delegate type.

Now, Action<object> and Action<string> are certainly instances of a different delegate type (even though related via inheritance relationship), so according to the documentation, it will throw an exception. It sounds reasonable that the Delegate.Combine method could support this scenario, but that's just a possible proposal (obviously this wasn't needed until now, because you cannot declare inherited delegates, so before co/contra-variance, no delegates had any inheritance relationship).



回答3:

One difficulty with delegate combination is that unless one specifies which operand is supposed to be the subtype and which is supertype, it's not clear what type the result should be. It is possible to write a wrapper factory which will convert any delegate with a specified number of arguments and pattern of byval/byref into a supertype, but calling such a factory multiple times with the same delegate would yield different wrappers (this could play havoc with event unsubscription). One could alternatively create a versions of Delegate.Combine which would coerce the right-side delegate to the left delegate's type (as a bonus, the return wouldn't have to be typecast), but one would have to write a special version of delegate.remove to deal with it.



回答4:

This solution was originally posted by cdhowie for my question: Delegate conversion breaks equality and unables to disconnect from event but is appears to solve the problem of covariance and contravariance in context of multicast delegates.

You first need a helper method:

public static class DelegateExtensions
{
    public static Delegate ConvertTo(this Delegate self, Type type)
    {
        if (type == null) { throw new ArgumentNullException("type"); }
        if (self == null) { return null; }

        if (self.GetType() == type)
            return self;

        return Delegate.Combine(
            self.GetInvocationList()
                .Select(i => Delegate.CreateDelegate(type, i.Target, i.Method))
                .ToArray());
    }

    public static T ConvertTo<T>(this Delegate self)
    {
        return (T)(object)self.ConvertTo(typeof(T));
    }
}

When you have a delegate:

public delegate MyEventHandler<in T>(T arg);

You can use it while combining delegates simply by converting a delegate do desired type:

MyEventHandler<MyClass> handler = null;
handler += new MyEventHandler<MyClass>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>();
handler += new MyEventHandler<object>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>();

handler(new MyClass());

It supports also disconnecting from event the same way, by using ConvertTo() method. Unlike using some custom list of delegates, this solution provides thread safety out of the box.

Complete code with some samples you can find here: http://ideone.com/O6YcdI