这个问题说明了一些关于逆变和协方差的有趣事实。
有两种方法可以理解这些问题。首先是抽象地看它,只看“箭头的方向”。
请记住,“协变”意味着变换保留可分配箭头的方向,而“逆变”意味着它是相反的。也就是说,如果 A --> B 的意思是“A类型的对象可以赋值给B类型的变量”,那么:
Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>
制作一个序列保留箭头的方向;它是“协变的”。 “Co”在这里的意思是“一起”。比较反方向,是“contra”,意思是“反对”。
这应该是有道理的;在需要一系列动物的情况下,可以使用一系列长颈鹿。如果你有一个可以比较任何动物的东西,那么它就可以比较任何长颈鹿。
理解为什么你的最后两个程序片段都是合法的方法是因为在你有两个嵌套的协变类型的情况下,你是在说“走同一个方向,然后走同一个方向as that”,同“同方向”。当你嵌套两个 逆变 类型时,你说的是“走相反的方向,然后走相反的方向”,这与“走同一个方向”相同!逆变换向箭头的方向。将箭头倒转两次会将其变回原来的方向!
但我不喜欢这样理解这些事情。相反,我喜欢思考“如果我们以另一种方式进行,会出现什么问题?”
那么让我们看看你的四个案例,然后问“会出什么问题”?
我会对你的类型做一些小的改动。
public delegate void D1<in T>(T t);
public delegate void D2<in T>(D1<T> d1t); // This is wrong.
为什么 D2 错了?好吧,如果我们允许的话会出什么问题?
// 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());
现在笼子里有老虎,这很好;笼子可以容纳任何动物。
但是现在让我们看看 D2 出了什么问题。假设 D2 的声明是合法的。
// 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());
好的,该程序中的每一行都是类型安全的。但要追溯逻辑。发生了什么?当我们在最后一行调用 d1tiger 时,它等于什么?好吧,d2fish(d1fish) 将 d1fish 分配给... d1tiger。但是 d1tiger 的类型是 D1<Tiger> 而不是 D1<Fish>。 所以我们给一个类型错误的变量赋值。然后发生了什么?我们用一只新老虎叫 d1Tiger,d1Tiger 把一只老虎放进了水族箱!
每一行都是类型安全的,但程序不是类型安全的,那么我们应该得出什么结论呢? D2 的声明不是类型安全的。这就是编译器给你一个错误的原因。
基于此分析,我们知道D2<in T>(D1<T>) 一定是错误的。
练习1:
delegate T D3<out T>();
delegate void D4<in T>(D3<T> d3t);
执行与我相同的逻辑,但这次,请说服自己这永远不会引起类型系统问题。
一旦你搞定了,那就做最难的:
练习 2:再次检查逻辑,但这次是
delegate void D5<in T>(D3<D3<T>> d3d3t);
再次说服自己这是合法的,并且这种情况在逻辑上与练习 1 相同。
练习 3:最后一个,最难的是:
delegate void D6<in T>(D1<D1<T>> d1d1t);
说服自己这是合法的,因为D1<D1<T>> 将箭头反转了两次,因此逻辑上与练习 1 相同。