covariance/contravariance C# 通用委托中的问题
covariance/contravariance problem in C# generic delegate
在下面的代码中有两个带有 covariance/contravariance 的通用委托声明:
// wrong code since Delegate1 actually needs covariance
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
要修复它,我们可以将 Delegate1 的声明调整为协方差
// ok
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
但是如果我将“Delegate2<in T>(Delegate1<T> d1)
”调整为“Delegate2<in T>(Delegate1<Delegate1<T>> d1)
”,下面的代码都可以(无论Delegate1
是协变还是逆变)
// 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);
我不太确定原因...
这个问题说明了一些关于逆变和协变的有趣事实。
有两种方法可以理解这些问题。第一种是抽象地看,只看"what direction the arrows go"。
请记住,"covariance" 表示转换 保留 可分配箭头的方向,而 "contravariance" 表示反转。即如果A-->B表示"An object of type A can be assigned to a variable of type B",则:
Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>
制作序列保留箭头的方向;它是 "co-variant"。 "Co" 意思是 "going with" 这里。做个比较反方向,就是"contra",意思是"going against".
这应该是有道理的;在需要一系列动物的地方可以使用一系列长颈鹿。如果你有一件事可以比较任何动物,那么它可以比较任何长颈鹿。
理解为什么你的最后两个程序片段都是合法的方法是因为在你有两个嵌套的 covariant 类型的情况下,你说的是 "go the same direction, then go the same direction as that",这与 "go the same direction" 相同。当你嵌套两个 contravariant 类型时,你说的是 "go the opposite direction, then go the opposite direction as that",这与 "go the same direction" 是一样的!逆变反转箭头的方向。将箭头反转 两次 将其转回原来的方向!
但这不是我喜欢的理解这些事情的方式。相反,我喜欢思考 "what could go wrong if we did it the other way?"
这个问题
那么让我们看看你的四个案例,然后问"what can go wrong"?
我将对您的类型做一些小改动。
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 相同。
在下面的代码中有两个带有 covariance/contravariance 的通用委托声明:
// wrong code since Delegate1 actually needs covariance
public delegate void Delegate1<in T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
要修复它,我们可以将 Delegate1 的声明调整为协方差
// ok
public delegate void Delegate1<out T>();
public delegate void Delegate2<in T>(Delegate1<T> d1);
但是如果我将“Delegate2<in T>(Delegate1<T> d1)
”调整为“Delegate2<in T>(Delegate1<Delegate1<T>> d1)
”,下面的代码都可以(无论Delegate1
是协变还是逆变)
// 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);
我不太确定原因...
这个问题说明了一些关于逆变和协变的有趣事实。
有两种方法可以理解这些问题。第一种是抽象地看,只看"what direction the arrows go"。
请记住,"covariance" 表示转换 保留 可分配箭头的方向,而 "contravariance" 表示反转。即如果A-->B表示"An object of type A can be assigned to a variable of type B",则:
Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>
制作序列保留箭头的方向;它是 "co-variant"。 "Co" 意思是 "going with" 这里。做个比较反方向,就是"contra",意思是"going against".
这应该是有道理的;在需要一系列动物的地方可以使用一系列长颈鹿。如果你有一件事可以比较任何动物,那么它可以比较任何长颈鹿。
理解为什么你的最后两个程序片段都是合法的方法是因为在你有两个嵌套的 covariant 类型的情况下,你说的是 "go the same direction, then go the same direction as that",这与 "go the same direction" 相同。当你嵌套两个 contravariant 类型时,你说的是 "go the opposite direction, then go the opposite direction as that",这与 "go the same direction" 是一样的!逆变反转箭头的方向。将箭头反转 两次 将其转回原来的方向!
但这不是我喜欢的理解这些事情的方式。相反,我喜欢思考 "what could go wrong if we did it the other way?"
这个问题那么让我们看看你的四个案例,然后问"what can go wrong"?
我将对您的类型做一些小改动。
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 相同。