質問

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.

役に立ちましたか?

解決

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:

  • If an object of reference type X may be assigned to a variable of reference type Y, then an object of type 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#

他のヒント

Consider this example:

string s = "Hello World!";
object o = s; // fine

If we wrap it around with Action

Action<string> s;
Action<object> o;
s = o; // reverse of T's polymorphism

This is because in parameter's effect is making the generic type's assignable hierarchy the reverse of the type parameter's hierarchy. For example, if this is valid:

TDerived t1; 
TBase t2; 
t2 = t1; // denote directly assignable without operator overloading

then

Action<TDerived> at1; 
Action<TBase> at2; 
at1 = at2; // contravariant

is valid. Then

Action<Action<TDerived>> aat1;
Action<Action<TBase>> aat2;
aat2 = aat1; // covariant

and also

Action<Action<Action<TDerived>>> aaat1;
Action<Action<Action<TBase>>> aaat2;
aaat1 = aaat2; // contravariant

and so on.

The effect and how covariant and contravariant works comparing to normal assignment are explained in http://msdn.microsoft.com/en-us/library/dd799517.aspx. In short, covariant assignment works like normal OOP polymorphism, and contravariant works backward.

Consider this:

class Base { public void dosth(); }
class Derived : Base { public void domore(); }

Using Action of T:

// this is all clear
Action<Base> a1 = x => x.dosth();
Action<Derived> b1 = a1;

Now:

Action<Action<Derived>> a = x => { x(new Derived()); };
Action<Action<Base>> b = a;
// the line above is basically this:
Action<Action<Base>> b = x => { x(new Derived()); };

Which would work, because you can still treat the result of the new Derived() as a Base. Both classes can dosth().

Now, in this case:

Action<Action<Base>> a2 = x => { x(new Derived()); };
Action<Action<Derived>> b2 = x => { x(new Derived()); };

It would still work, when using new Derived(). However, that cannot be said generally and is thus illegal. Consider this:

Action<Action<Base>> a2 = x => { x(new Base()); };
Action<Action<Derived>> b2 = x => { x(new Base()); };

Error: Action<Action<Derived>> expects domore() to exist, but Base only delivers dosth().

There is an inheritance tree, with object as the root. A path on the tree would normally look something like this

object -> Base -> Child

An Object of a type higher on the tree can allways be assigned to a variable of a type lower on the tree. Covariance in generic types means the realized types are related in a way that follows the tree

object -> IEnumerable<object> -> IEnumerable<Base> -> IEnumerable<Child>

object -> IEnumerable<object> -> IEnumerable<IEnumerable<object> -> ...

Contravariance means the realized types are related in a way that reverses the tree.

object -> Action<Child> -> Action<Base> -> Action<object>

When you go a level deeper, you have to reverse the tree again

object -> Action<Action<object>> -> Action<Action<Base>> -> Action<Action<Child>> -> Action<object>

p.s. with contravariance, the object hierarchy is no longer a tree, but is in fact a directed acyclic graph

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top