【问题标题】:covariance/contravariance problem in C# generic delegateC#泛型委托中的协变/逆变问题
【发布时间】:2019-01-24 04:58:55
【问题描述】:

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

// 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&lt;in T&gt;(Delegate1&lt;T&gt; d1)”调整为“Delegate2&lt;in T&gt;(Delegate1&lt;Delegate1&lt;T&gt;&gt; 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】:

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

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

    请记住,“协变”意味着变换保留可分配箭头的方向,而“逆变”意味着它是相反的。也就是说,如果 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&lt;Tiger&gt; 而不是 D1&lt;Fish&gt; 所以我们给一个类型错误的变量赋值。然后发生了什么?我们用一只新老虎叫 d1Tiger,d1Tiger 把一只老虎放进了水族箱!

    每一行都是类型安全的,但程序不是类型安全的,那么我们应该得出什么结论呢? D2 的声明不是类型安全的。这就是编译器给你一个错误的原因。

    基于此分析,我们知道D2&lt;in T&gt;(D1&lt;T&gt;) 一定是错误的。

    练习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&lt;D1&lt;T&gt;&gt; 将箭头反转了两次,因此逻辑上与练习 1 相同。

    【讨论】:

    • 我和刚开始阅读时一样困惑,但其他方面的信息非常好。
    • @ColinM:如果您有关于方差的具体问题可能会让您感到困惑,请随时在评论中发布指向它的链接,我会在有时间时查看。人们经常发现这个主题很混乱,尤其是在涉及逆变时。当你大声说出来时,“将箭头反转两次就是保留箭头”的想法似乎很明显,但在代码中对此进行推理却很棘手。
    • 更多的是我必须坐下来更深入地理解它并花时间处理 - 可能是喝咖啡,这是我从未真正掌握概念的一件事,但是是很多关于协/逆变的有价值的在线信息,主要由您发布。我很感激这个提议
    • @DaveCousineau:我不知道非标准空间是如何粘贴到那里的。好收获!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-29
    • 2021-10-24
    相关资源
    最近更新 更多