This is something I'm having a hard time wrapping my head around. I understand that Action<T>
is contravariant and is probably declared as such.
internal delegate void Action<in T>(T t);
However, I don't understand why an Action<Action<T>>
is covariant. T
is still not in an output position. I'd really appreciate it if someone could try to explain the reasoning / logic behind this.
I dug around a little bit and found this blog post which tries to explain it. In particular, I didn't quite follow what was meant here under the "Explanation for covariance of input" subsection.
It is the same natural if the “Derived -> Base” pair is replaced by “Action -> Action” pair.
In object-oriented programming, a covariant return type of a method is one that can be replaced by a "narrower" type when the method is overridden in a subclass. A notable language in which this is a fairly common paradigm is C++. C# supports return type covariance as of version 9.0.
In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.
Covariance can be translated as "different in the same direction," or with-different, whereas contravariance means "different in the opposite direction," or against-different. Covariant and contravariant types are not the same, but there is a correlation between them. The names imply the direction of the correlation.
In short, type variance describes the types that may be substituted in place of another type. Covariance: We say a substitution is covariant with a type, Foo, if Foo or any other class with a subclass relationship to Foo is valid.
OK, so first of all let's be clear what you mean by saying that Action<Action<T>>
is covariant. You mean that the following statement holds:
Action<Action<X>>
may be assigned to a variable of reference type Action<Action<Y>>
.Well, let's see if that works. Suppose we have classes Fish
and Animal
with the obvious inheritance.
static void DoSomething(Fish fish)
{
fish.Swim();
}
static void Meta(Action<Fish> action)
{
action(new Fish());
}
...
Action<Action<Fish>> aaf = Meta;
Action<Fish> af = DoSomething;
aaf(af);
What does that do? We pass a delegate to DoSomething to Meta. That creates a new fish, and then DoSomething makes the fish swim. No problem.
So far so good. Now the question is, why should this be legal?
Action<Action<Animal>> aaa = aaf;
Well, let's see what happens if we allow it:
aaa(af);
What happens? Same thing as before, obviously.
Can we make something go wrong here? What if we pass something other than af
to aaa
, remembering that doing so will pass it along to Meta
.
Well, what can we pass to aaa
? Any Action<Animal>
:
aaa( (Animal animal) => { animal.Feed(); } );
And what happens? We pass the delegate to Meta
, which invokes the delegate with a new fish, and we feed the fish. No problem.
T is still not in an output position. I'd really appreciate it if someone could try to explain the reasoning / logic behind this.
The "input/output" position thing is a mnemonic; covariant type tend to have the T in the output position and contravariant type tend to have the T in the input position, but that is not universally true. For the majority of cases, that's true, which is why we chose in
and out
as the keywords. But what really matters is that the types can only be used in a typesafe manner.
Here's another way to think about it. Covariance preserves the direction of an arrow. You draw an arrow string --> object
, you can draw the "same" arrow IEnumerable<string> --> IEnumerable<object>
. Contravariance reverses the direction of an arrow. Here the arrow is X --> Y
means that a reference to X may be stored in a variable of type Y:
Fish --> Animal
Action<Fish> <-- Action<Animal>
Action<Action<Fish>> --> Action<Action<Animal>>
Action<Action<Action<Fish>>> <-- Action<Action<Action<Animal>>>
...
See how that works? Wrapping Action
around both sides reverses the direction of the arrow; that's what "contravariant" means: as the types vary, the arrows go in the contra -- opposing -- direction. Obviously reversing the direction of an arrow twice is the same thing as preserving the direction of an arrow.
FURTHER READING:
My blog articles that I wrote while designing the feature. Start from the bottom:
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx
A recent question about how variance is determined to be typesafe by the compiler:
Variance rules in C#
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With