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

时间:2019-01-24 04:58:55

标签: c# 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);

我不太清楚原因...

1 个答案:

答案 0 :(得分:7)

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

有两种方法可以了解这些问题。首先是抽象地看它,而只是看“箭头的方向”。

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

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

制作序列保留箭头的方向;它是“协变量”。 “ Co”在这里的意思是“一起去”。进行比较会使方向相反,即“相反”,表示“违背”。

这应该说得通;如果需要一系列动物,可以使用一系列长颈鹿。而且,如果您有可以比较任何动物的东西,那么它就可以比较任何长颈鹿。

理解为什么最后两个程序片段都合法的原因是,在具有两个嵌套的 covariant 类型的情况下,您说的是“朝同一个方向,然后朝同一个方向那样”,与“朝同一方向”相同。当嵌套两个 contravariant 类型时,您说的是“朝相反的方向,然后朝相反的方向”,这与“朝相同的方向”相同!反转会反转箭头的方向。反转箭头两次可以将其恢复原来的状态!

但这不是我喜欢理解这些东西的方式。而是,我想考虑一个问题“如果我们反过来做会怎么办?”

那么让我们看一下您的四个案例,并问“可能出什么问题”吗?

我将对您的类型进行一些小的更改。

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