Search code examples
c#.netgenericsdelegatescontravariance

Why can't I pass a lambda expression with a less-derived parameter type to a variable of type Action<T> given that the latter is contravariant on T?


Let's say I have these classes:

class Animal { }
class Cat : Animal { }

And then I declare a variable like this:

Action<Cat> c;

Now, Action<T> is contravariant on T, so I can do this:

void Foo(Animal a) { }
c = Foo;

That makes sense. But when I do this, I get compiler error CS16611:

c = (Animal a) => { };

I can even do this with no error:

Action<Animal> b = (Animal a) => { };
c = b;

What am I missing here?


Solution

  • Super short version: you can tell the compiler which delegate type you want it to use for your expression using casting:

    c = (Action<Animal>)(a => { });
    

    Right, that aside..

    As madreflection pointed out while I was writing this (thanks mad), the problem is that pure delegates are not contravariant. Contravariance works only for generic delegates of the same generic type.

    Consider this list of items:

    // Let's start with defining some delegate types:
    delegate void AnimalAction(Animal parm);
    delegate void MyAction<in T>(T parm);
    
    // Now here are my variables, all working fine:
    Action<Animal> actAnimal = (Animal a) => { };
    MyAction<Animal> myAnimal = (Animal a) => { };
    AnimalAction animalAction = (Animal a) => { };
    

    They all look basically the same, and indeed are all functionally identical. Sadly no matter how similar they look, and no matter the fact that the method void DoNothing(Animal parm) { } can be assigned to all of them, the compiler will tell you that they're not the same.

    So what happens when we have a lambda expression like (Animal a) => { }? Well all of the above will accept it because the compiler is bright enough to figure out what you're doing.

    Now let's change it up:

    // New delegate for cats
    delegate void CatAction(Cat parm);
    
    // New variables, but these all fail:
    Action<Cat> actCat = (Animal a) => { };
    MyAction<Cat> myCat = (Animal a) => { };
    CatAction catAction = (Animal a) => { };
    

    The compiler objects in all of these cases because it the transitional type of the lambda expression (delegate _expression_type(Animal a)) is not reference-convertable to the various target delegate types.

    You can get around this in a few ways however. You can wrap one delegate in another of compatible type using new Action<Cat>(some_animal_action). Or you can simply tell the compiler to make the right type from the beginning, like I put at the top.

    So these all work:

    c = (Action<Animal>)(a => { });
    c = new Action<Animal>(a => { });
    c = new Action<Animal>((Animal a) => { });
    

    (But don't do the new versions, it will probably create a nested delegate.)

    Oddly, you can also capture the output in an auto-typed variable using var. This works because the compiler knows to use Action<...> and Func<...> for expression delegates, but only when the delegate's type is not fully specified beforehand. So this works too:

    var temp = (Animal a) => { };
    
    // 'temp' is of type Actions<Animal> so this works:
    c = temp;