Reputation: 111
In the below code there are two generic delegate declarations with covariance/contravariance :
// wrong code since Delegate1 actually needs covariance
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
to fix it, we can adjust Delegate1's declaration to covariance
// ok
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
but if I adjust "Delegate2<in T>(Delegate1<T> d1)
" to "Delegate2<in T>(Delegate1<Delegate1<T>> d1)
", code below will be both OK(whether Delegate1
is covariance or contravariance)
// ok
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<Delegate1<T>> d1);
// ok too
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<Delegate1<T>> d1);
I'm not so sure about the reason ...
Upvotes: 1
Views: 286
Reputation: 660327
This question illustrates some interesting facts about contravariance and covariance.
There are two ways to understand these problems. The first is to look at it abstractly and just look at "what direction the arrows go".
Remember that "covariance" means that a transformation preserves the direction of the assignability arrow and "contravariance" means it is reversed. That is, if A --> B means "An object of type A can be assigned to a variable of type B", then:
Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>
Making a sequence preserves the direction of the arrow; it is "co-variant". "Co" meaning "going with" here. Making a comparison reverses the direction, it is "contra", meaning "going against".
This should make sense; a sequence of giraffes can be used where a sequence of animals is needed. And if you have a thing that can compare any animals, then it can compare any giraffes.
The way to understand why your last two program fragments are both legal is because in the case where you have two nested covariant types, you are saying "go the same direction, then go the same direction as that", which is the same as "go the same direction". When you nest two contravariant types, you are saying "go the opposite direction, then go the opposite direction as that", which is the same as "go the same direction"! Contravariance reverses the direction of an arrow. Reversing the arrow twice turns it back the way it was facing originally!
But that is not how I like to understand these things. Rather, I like to think about the question "what could go wrong if we did it the other way?"
So let's look at your four cases and ask "what can go wrong"?
I'll make some small changes to your types.
public delegate void D1<in T>(T t);
public delegate void D2<in T>(D1<T> d1t); // This is wrong.
Why is D2 wrong? Well, what can go wrong if we allowed it?
// This is a cage that can hold any animal.
AnimalCage cage = new AnimalCage();
// Let's make a delegate that inserts an animal into the cage.
D1<Animal> d1animal = (Animal a) => cage.InsertAnimal(a);
// Now lets use the same delegate to insert a tiger. That's fine!
D1<Tiger> d1tiger = d1animal;
d1tiger(new Tiger());
Now there is a tiger in cage, which is fine; the cage can hold any animal.
But now let's see how things go wrong with D2. Let's suppose that the declaration of D2 was legal.
// This line is fine; we're assigning D1<Animal> to D1<Tiger>
// and it is contravariant.
D2<Animal> d2animal = (D1<Animal> d1a) => {d1tiger = d1a;};
// An aquarium can hold any fish.
Aquarium aquarium = new Aquarium();
// Let's make a delegate that puts a fish into an aquarium.
D1<Fish> d1fish = (Fish f) => aquarium.AddFish(f);
// This conversion is fine, because D2 is contravariant.
D2<Fish> d2fish = d2animal;
// D2<Fish> takes a D1<Fish> so we should be able to do this:
d2fish(d1fish);
// Lets put another tiger in the cage.
d1tiger(new Tiger());
OK, every line in that program was type safe. But trace through the logic. What happened? When we called d1tiger on the last line, what did it equal? Well, d2fish(d1fish) assigns d1fish to... d1tiger. But d1tiger is typed as D1<Tiger>
not D1<Fish>
. So we've assigned a value to a variable of the wrong type. Then what happened? We called d1Tiger with a new tiger, and d1Tiger put a tiger into an aquarium!
Every one of those lines was typesafe, but the program was not typesafe, so what should we conclude? The declaration of D2 was not typesafe. And that's why the compiler gives you an error.
Based on this analysis we know that D2<in T>(D1<T>)
has to be wrong.
Exercise 1:
delegate T D3<out T>();
delegate void D4<in T>(D3<T> d3t);
Go through the same logic I did, but this time, convince yourself that this never gives rise to a type system problem.
Once you've got that down, then do the hard ones:
Exercise 2: Go through the logic again, but this time with
delegate void D5<in T>(D3<D3<T>> d3d3t);
Again, convince yourself that this is legal, and that this case is logically the same as Exercise 1.
Exercise 3: And the last, hardest one is:
delegate void D6<in T>(D1<D1<T>> d1d1t);
Convince yourself that this is legal because D1<D1<T>>
reverses the arrow twice, and is therefore logically the same as Exercise 1.
Upvotes: 7