Covariance and Contravariance in C#, Part Three: Method Group Conversion Variance

Covariance and Contravariance in C#, Part Three: Method Group Conversion Variance

Rate This
  • Comments 14

Last time I discussed how array covariance is broken in C# (and Java, and a number of other languages as well.) Today, a non-broken kind of variance supported by C# 2.0: conversions from method groups to delegates. This is a more complicated kind of variance, so let me spell it out in more detail.

Suppose that you have a method which returns a Giraffe:

static Giraffe MakeGiraffe() { …

Suppose further that you have a delegate type representing a function which takes no arguments and returns an Animal. Say, Func<Animal>. Should this implicit conversion from method group to delegate be legal?

Func<Animal> func = MakeGiraffe;

The caller of func is expecting an Animal to be returned. The actual function captured by the delegate always returns a Giraffe, which is an Animal, so the caller of func is never going to get anything that they’re not capable of dealing with. There is no problem in the type system here. Therefore we can make method group to delegate conversions covariant (‡) in their return types.

Now suppose you have two methods, one which takes a Giraffe and one which takes an Animal:

void Foo(Giraffe g) {}
void Bar(Animal a) {}

and a delegate to a void-returning function that takes a Mammal:

Action<Mammal> action1 = Foo; // illegal
Action<Mammal> action2 = Bar; // legal

Why is the first assignment illegal? Because the caller of action1 can pass a Tiger, but Foo cannot take a Tiger, only a Giraffe! The second assignment is legal because Bar can take any Animal.

In our previous example we preserved the direction of the assignability: Giraffe is smaller than Animal, so a method which returns a Giraffe is smaller than a delegate which returns an Animal. In this example we are reversing the direction of the assignability: Mammal is smaller than Animal, so a method which takes an Animal is smaller than a delegate which takes a Mammal. Because the direction is reversed, method group to delegate conversions are contravariant in their argument types.

Note that all of the above applies only to reference types. We never say something like “well, every int fits into a long, so a method which returns an int is assignable to a variable of type Func<long>”.

Next time: a stronger kind of delegate variance that we could support in a hypothetical future version of C#.

(‡) A note to nitpickers out there: yes, I said earlier that variance was a property of operations on types, and here I have an operation on method groups, which are typeless expressions in C#. I’m writing a blog, not a dissertation; deal with it!

  • Hi Eric.

    Great post!

    Are you going to support some kind of variance of generics in a future version of C#?

    Thanks.

  • Hi nikov, good to hear from you.

    Once again, I do not make promises about features of unannounced products, blah blah blah.  

    But hypothetically, if there were to be a future version of C#, and we were to support variance on generics, then what would we support?

    I'll be discussing that in more detail in the next few posts, but the short answer is "covariance and contravariance on generic interfaces and generic delegates, where both must be parameterized by reference types".

    Again, I want to emphasize that this is not a promise, this is a discussion. I'm floating a trial balloon here to see what people think of the feature.

    More details coming up next week.

  • Eric,

    I *think* there's another type of covariance/contravariance of delegates which is new to C# 2 as well. Consider the following code:

    using System;

    using System.ComponentModel;

    class Test

    {

       static void Handler(object o, EventArgs e)

       {

       }

       static void Main()

       {

           EventHandler x = Handler;

           CancelEventHandler y = new CancelEventHandler(x);

       }

    }

    I *believe* that would fail to compile under C# 1, but it works with C# 2.

    In C# 1 (ECMA 334, 2nd edition) delegate types are only compatible if they have the same parameter list and the same return type.

    Jon

  • Welcome to the Thirty-Fourth issue of Community Convergence. This is a time when the team is in transition.

  • Welcome to the Thirty-Fourth issue of Community Convergence. This is a time when the team is in transition.

  • Hi Eric,

    this regards probably your previous post, but this variance is broken:

           private static IEnumerable<object> O() { return new object[0]; }

           private static IEnumerable<string> S() { return new string[0]; }

            Func<IEnumerable<object>> a;

             a = O;

             a = S; //illegal

    and I believe that an Enumeration of camels is an Enumeration of mamals :D

    regards,

    tap.

  • Hi,

    This is a little different but reminded me of something I ran into earlier that is demonstrated in the snippet below

           private delegate int IntMethod();

           public int MethodOne()

           {

               return 1;

           }

           public int MethodTwo()

           {

               return 2;

           }

           public void Test()

           {

               IntMethod one = MethodOne;

               IntMethod two = MethodTwo;

               bool useOne=true;

               IntMethod pick1 = useOne ? one : two; //works fine

               IntMethod pick2 = useOne ? (IntMethod)MethodOne : (IntMethod)MethodTwo; //works fine

               IntMethod pick3 = useOne ? MethodOne : MethodTwo; //ERROR!

              // Error 18 Type of conditional expression cannot be determined because there is

              // no implicit conversion between 'method group' and 'method group'

           }

    Can you explain why the compiler is not able to work with the last expression?

    Sayed Ibrahim Hashimi

  • We need to know what the types of the consequence and alternative of the condition are, and in your example without type casts, we do not know the types of either.  We try to convert the consequence to the type of the alternative, and vice versa, but since neither has a type, the conversion fails.

  • So nicely step by step blogged by Eric Lippert for &quot;Covariance and Contravariance&quot; as &quot;Fabulous

  • In the last two posts I discussed the two kinds of variance that C# already has -- array covariance and

  • <<Note that all of the above applies only to reference types. We never say something like “well, every int fits into a long, so a method which returns an int is assignable to a variable of type Func<long>”>>

    It seems an int is not even an object either :). Is your statement rather saying "This never applies to value types". If so - can you please explain why?

    delegate void doSomething<T>(T obj);

    void foo()
    {
        //refs types compiles
        doSomething<string> bar1 = dontGetIt;
         //value types don't compile
         doSomething<int> bar2 = dontGetIt;
    }
    void dontGetIt(object foo){}

    Every reference is the same; every value is different. A reference to an object, whether it is a string or an Exception, is the same on a given architecture; say, a 64 bit number that represents a location in the gc heap. An int is a 32 bit hunk of bits that represents an int itself, not a location where an int is stored. To use a value as a reference, it has to be boxed; a location has to be made for it on the heap and the contents copied into that location, and then a reference to that location is used. So suppose you have your method that takes an object. It's body expects that there will be a 64 bit reference passed in. Now suppose you call bar2(123). It passes in a 32 bit integer to a method that expects a 64 bit reference. What code boxes the int? No code boxes the int, that's what. And that's why this is illegal; we'd either make it legal to misalign the stack and crash the runtime horribly, or we'd have to generate special helper code that runs around allocating memory and doing copies on your behalf, at which point we are no longer doing an identity-preserving conversion between two different delegate types, we are creating a brand-new delegate object that does something different. -- Eric

  • Thanks for your answer! I get it... maybe. What puzzles me is the difference compared to a direct call to the method? In that case the int would be boxed in the background for me. In this case, where a (generic) delegate is used, no boxing is done for me.

    I'm aware/think you explain this for me in your last sentence. I read it twenty times now. I think I need to read twenty times more :)

    Suppose you have M(object) and you call it M(123). The compiler says "Ah, I see there is a call to M(object) with a value type, I will generate the code M(new Box<int>(123))".  Now suppose you have delegate void D<T>(in T t) and you say D<object> d = M; D<int> e = d; e(123);"  What does the compiler say now? It says "Ah, I see there is a call to D<int>.Invoke(123). Since that method already takes an integer, I won't box it."  But the delegate is to M, which requires a boxed int. At some point the compiler has to generate a boxing instruction for that call, but you have hidden that fact from the compiler. Now, what the compiler could do is say "ah, I see there is a conversion from D<object> to D<int>, I will actually generate e = (int x)=>{d(new Box<int>(x));}", and there's the boxing instruction. But if we did this then e and d would no longer be *equal*. They wouldn't be the same object. We want the contravariant conversion to give you object identity, and we can only do that if the conversion preserves representational identity at every stage along the way. Make more sense now? -- Eric

  • Yes! Brilliant - thanks!

    /Roger, Sweden

  • I think I get it... So in line with your statement, 'At runtime you can store an object which is an instance of an equal or smaller type in that storage location.', the LHS of an assignment will always be bigger than the right even if this means that Action<object> is considered smaller than Action<string>.

Page 1 of 1 (14 items)