2

It has the following interface:

public interface ICovariant<out T>
{
    public void InputFunc(Func<T> func);
}

I get the following compile-time error:

Invalid variance: covariant type parameter 'T' is used in contravariant position. Parameter must be input-safe

The location of Func<T> is input. Func<T> is Func<out T>, so it is output.

OK, I was wrong. Get information that location is relevant.

However, when I add the following method, an error occurs.

public interface ICovariant<out T>
{
    public void InputFunc(Func<T> func);
    public Action<T> ReturnAction();
}

Invalid variance: covariant type parameter 'T' is used in contravariant position. Method return type must be output-safe

The location of Action<T> is the output. Action<T> is Action<in T>, so it is input.

what? Unlike before, this time the location doesn't matter.

Why do the two examples perceive each other differently?

isakgo_
  • 750
  • 4
  • 15

2 Answers2

12

Sweeper's accepted answer is good; I want to provide a slightly different way to think about why your proposed program is illegal. Before I do that, once again allow me to apologize for the low quality of the error messages here. I couldn't come up with anything better and apparently they have not been improved much in the subsequent ~15 years.

public interface ICovariant<out T>
{
    public Action<T> ReturnAction(); // Why is this illegal?
}

Why is this illegal? Try to implement it and then see what goes wrong!

class Animal {...}
class Mammal : Animal {...}
// ... and so on ...
class Danger: ICovariant<Mammal>
{
  public Action<Mammal> ReturnAction()
  {
    return (Mammal m) => { m.ProduceMilk(); };
  }
}

Danger meets the requirements of ICovariant<Mammal> -- it returns an action, that action takes a Mammal, and it does a thing -- makes the mammal produce milk:

ICovariant<Mammal> danger = new Danger();
Action<Mammal> action = danger.ReturnAction();
action(new Goat()); // Goat milk, mmmmm

But ICovariant<Mammal> is convertible to ICovariant<Animal> because its covariant, so we could also write:

ICovariant<Animal> danger = new Danger();
// ICov<Animal> produces an Action<Animal>
Action<Animal> action = danger.ReturnAction();
action(new Chicken());

And now we have milked a chicken. Nothing good will come of the attempt.

The lines above are perfectly legal; they cannot be an error. To prevent this situation from happening, the compiler has to make the interface itself illegal.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
3

Unlike before, this time the location doesn't matter.

The location does matter! This compiles perfectly:

public interface ICovariant<out T>
{
    public void AcceptAction(Action<T> action);
}

What the contravariant type parameter in Action does, is that it makes its corresponding type argument have the opposite position of where you have put it - if it is originally in an input position, it is now in an output position, and if it is originally in an output position, it is now in an input position.

"Contra-" means "opposite", so that's an easy to remember this behaviour :D Though this particular behaviour is probably not where the name "contravariance" comes from. Eric Lippert's answer here explains why it's called that.

So this compiles too (the position of T gets "flipped" twice, and so it is back to the output position):

public Action<Action<T>> ReturnsNestedAction();

If you just think about how you would actually call ReturnAction, you should be able to intuitively convince yourself that, indeed, T in ReturnAction is in an input position:

Action<T> action = someICovariant.ReturnAction();
action(someT); // I am *inputting* an instance of T here!

Likewise, you can do the same for AcceptAction and see that T is indeed in an output position there:

someICovariant.AcceptAction(t => {
    Console.WriteLine(t);
});
/*
Imagine the implementation of AcceptAction invoking its parameter at some point.
That is analogous to AcceptAction *returning* some instance of T and we print it out
The difference being that instead of "return someT;", 
AcceptAction would do "action(someT);", but the fact it is producing an instance of T
doesn't change
*/

If you are interested in how this is specified in technical terms, you can go read this section of the language specification.

See also this related question about similar "flipping positions" behaviours when declaring a contravariant type parameter.

Sweeper
  • 213,210
  • 22
  • 193
  • 313