C#泛型委托中的协方差/逆变问题

问题描述 投票:1回答:1

在下面的代码中,有两个具有协方差/逆变的通用委托声明:

// 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);

我不太确定原因......

c# covariance contravariance
1个回答
7
投票

这个问题说明了一些关于逆变和协方差的有趣事实。

有两种方法可以理解这些问题。首先是抽象地看待它,然后看看“箭头走向的方向”。

请记住,“协方差”意味着转换保留了可分配性箭头的方向,“逆变”意味着它是相反的。也就是说,如果A - > B表示“类型A的对象可以分配给类型B的变量”,则:

Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>

制作序列可保留箭头的方向;它是“共同变体”。 “Co”意思是“在这里”。进行比较会改变方向,它是“反对”的,意思是“反对”。

这应该是有道理的;在需要一系列动物的地方可以使用一系列长颈鹿。如果你有一个可以比较任何动物的东西,那么它可以比较任何长颈鹿。

理解为什么你的最后两个程序片段都合法的方法是因为在你有两个嵌套的协变类型的情况下,你说的是“走同一个方向,然后走同样的方向”,这与“走同一个方向“。当你嵌套两个逆变类型时,你会说“走向相反的方向,然后走向相反的方向”,这与“走同一个方向”相同!逆变会逆转箭头的方向。将箭头反转两次会使其恢复原来的方式!

但这不是我喜欢理解这些事情的方式。相反,我喜欢思考“如果我们以另一种方式做到这一点会出现什么问题?”

那么让我们看看你的四个案例并问“什么可能出错”?

我会对你的类型做一些小改动。

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相同。

© www.soinside.com 2019 - 2024. All rights reserved.