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.