36

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.

controlflow
  • 6,530
  • 29
  • 54
  • sorry, I've switched keyboard locale wrong place... – controlflow Feb 22 '10 at 09:10
  • @ControlFlow - See http://stackoverflow.com/questions/1120688/event-and-delegate-contravariance-in-net-4-0-and-c-4-0 - I noticed this problem in the first beta of VS2010. At that point, `EventHandler` was contravariant w.r.t. `TEventArgs`. But since then it has been changed back as you've found. – Daniel Earwicker Mar 01 '10 at 13:03
  • Argh! I accidentally downvoted your question (I meant to upvote it) and now it's too old to change unless you edit it! – Daniel Earwicker Mar 01 '10 at 13:08
  • @Earwicker Very interesting! To me this situation looks more and more like a hiding a bug with the `Delegate.Combine`... – controlflow Mar 01 '10 at 15:52
  • Indeed, but I think it's a perfectly reasonable workaround. And I think variance is such a brilliant feature that I was tempted to keep quiet when I saw this technical hitch, in case it got pulled! :) Anyway you've reminded me to finally blog about this: http://smellegantcode.wordpress.com/2010/03/01/that-unfortunate-interaction-between-multicast-delegates-and-contravariance-in-c-4-0/ – Daniel Earwicker Mar 01 '10 at 17:17
  • There is a similar question: http://stackoverflow.com/q/1120688/27343 – Stefan Steinegger Oct 05 '12 at 07:31

4 Answers4

38

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.

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
  • 20
    Eric your answers and especially their candidacy are very appreciated since very few of us (I would assume) has any idea of what happens internally at Microsoft with .NET and your information frequently makes the internals alot less of a black box to myself and the rest of the community here (obvious with your 28K rep). – Chris Marisic Feb 21 '10 at 23:46
  • 5
    @Chris: thanks! I'm all about making this whole process more transparent. – Eric Lippert Feb 22 '10 at 02:14
  • 1
    Has anything happened in this area since .net 4.0? `EventHandler` is still not marked as contra-variant in the 4.5 preview documentation. – CodesInChaos Dec 19 '11 at 12:41
  • 1
    @EricLippert: Would there be any problem having delegate types implement their own static `Combine` methods, rather than just inheriting `Delegate.Combine`? Not only would having `Action.Combine()` return an `Action` rather than `System.Delegate` avoid a typecast, but both `Action.Combine()` and `Action.Combine()` could accept an `Action` and an `Action` if both `Foo1` and `Foo2` implement both `IFoo` and `IBar`. Note that `Delegate.Combine` would not be able to do such a combination since it wouldn't know whether to return an `Action` or `Action` – supercat Aug 27 '12 at 01:38
  • 1
    C# 5 still exhibit this behaviour. – nawfal Jul 07 '14 at 16:46
  • 1
    C# 6 too. Anyone know if a fix is coming? – Nick Strupat Dec 02 '15 at 22:07
6

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).

Tomas Petricek
  • 225,798
  • 19
  • 345
  • 516
  • Yeah, I've right about documentation and `Delegate.Combine()` behavior... But about supporting this scenario: why your are talking about delegates inheritance? For my scenario generic parameters should has inheritance relationship, not delegate types itself. For example, subscribe to an `EventHandler with an `EventHandler` delegate instance. – controlflow Feb 21 '10 at 20:16
  • More precisely, I should have been talking about subtyping (that is, when you can treat value of type `A` as a value of type `B`). Covariance and contravariance define subtyping relationship between generic delegates (based on a subtyping/inheritance relationship between their type parameters). You can convert any subtype (inherited) to a supertype (base or interface) and covariance between delegates uses the same conversion rule. However, that doesn't quite extend to `Delegate.Combine` which probably uses internals of the delegate given as an argument. – Tomas Petricek Feb 21 '10 at 22:13
  • That was documented even before generic variance coming into play where signature of `Delegate.Combine(Delegate, Delegate)` takes any two `Delegate` types, so such an exception was valid to document. That documentation is misleading in the context of variance here considering this happens only when `a` already has a delegate of different type. – nawfal Jul 08 '14 at 05:42
  • For example, this will give no exception `Action act = null; act += new Action(x => x.ToString());` or even `Action act = new Action(x => x.ToString()); act += new Action(x => x.ToString());` even though in both cases `a` is `Action` and `b` is `Action` – nawfal Jul 08 '14 at 05:43
1

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.

supercat
  • 69,493
  • 7
  • 143
  • 184
0

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

Community
  • 1
  • 1
Kędrzu
  • 2,265
  • 11
  • 21