为什么C#在带有委托的输入参数中使用逆变(非协方差)?

pro*_*eek 4 c# delegates covariance contravariance

当我们有一个继承BBase的Base类和一个专门化它的Derived类时,假设有一个委托需要Base作为输入.

using System;

class BBase {}
class Base : BBase {}
class Derived : Base {}

delegate void BaseDelegate(Base b);
Run Code Online (Sandbox Code Playgroud)

在委托的使用中,不允许使用它BaseDelegate b2 = TakeDerived;作为输入是逆变的.

class MainClass
{
    static void TakeBBase(BBase bb) {}
    static void TakeBase(Base b) {}
    static void TakeDerived(Derived d) {}

    static void Main(string[] args)
    {
        BaseDelegate b1 = TakeBase;
        b1(new Derived());
        b1(new Base());

        // ERROR
        // parameters do not match delegate 
        // `BaseDelegate(Base)' parameters
        // The contract of b2 is to expect only Base
        //BaseDelegate b2 = TakeDerived;
Run Code Online (Sandbox Code Playgroud)

TakeBBase可以分配给BaseDelegate.

    BaseDelegate b2 = TakeBBase;
    b2(new Derived());
    b2(new Base());
Run Code Online (Sandbox Code Playgroud)

看到我们可以将Base类的子类分配给委托中的Base类型参数也很有趣.协方差/逆变规则似乎在前面的例子中不起作用.

  • 为什么C#选择在委托中的输入参数中使用逆变(非协方差)?
  • 当协方差/逆变规则适用于C#时?除代表之外还有哪些其他情况使用协方差/逆变?为什么?

Eri*_*ert 13

奥利维尔的回答是正确的; 我想我可能会尝试更直观地解释这一点.

为什么C#选择在委托中的输入参数中使用逆变(非协方差)?

因为逆变是类型安全的,所以协方差不是.

而不是基地,让我们说哺乳动物:

delegate void MammalDelegate(Mammal m);
Run Code Online (Sandbox Code Playgroud)

这意味着"一种需要哺乳动物而不会返回任何东西的功能".

所以,假设我们有

void M(Giraffe x)
Run Code Online (Sandbox Code Playgroud)

我们可以将其作为哺乳动物代表使用吗?不可以.哺乳动物代表必须能够接受任何哺乳动物,但M不接受猫,它只接受长颈鹿.

void N(Animal x)
Run Code Online (Sandbox Code Playgroud)

我们可以将其作为哺乳动物代表使用吗?是.哺乳动物的代表必须能够接受任何哺乳动物,N确实接受任何哺乳动物.

在这个例子中,协方差/逆变规则似乎不起作用.

这里没有任何差异.您正在犯一个极为常见的错误,即将赋值兼容性协方差混淆.赋值兼容性不是协方差.协方差是类型系统转换保留赋值兼容性的属性.

让我再说一遍.

你有一个采取哺乳动物的方法.你可以把它传给长颈鹿. 这不是协方差.这是赋值兼容性.该方法具有Mammal类型的形式参数.这是一个变量.你有Giraffe类型的值.该值可以分配给该变量,因此它与赋值兼容.

如果不是赋值兼容性,则变化是什么?我们来看一两个例子:

长颈鹿与哺乳动物的变量兼容.因此,一系列长颈鹿(IEnumerable<Giraffe>)的分配与哺乳动物的类型变量()的变量兼容IEnumerable<Mammal>.

这是协方差.协方差是我们可以从两种其他类型的赋值兼容性中推导出两种类型的赋值兼容性.我们知道可以将长颈鹿分配给动物类型的变量; 这让我们推断出另外两种类型的赋值兼容性事实.

你的代表示例:

哺乳动物的分配与动物类型的变量兼容.因此,采用动物方法与赋予哺乳动物的代表类型的变量兼容.

这是逆转.反演也是如此,我们可以推导出两件事的分配兼容性 - 在这种情况下,一个方法可以分配给特定类型的变量 - 来自两个其他类型的赋值兼容性.

协方差和逆变之间的区别仅仅在于"方向"是交换的.通过协方差,我们知道A = B合法的含义I<A> = I<B>是合法的.有了逆转,我们知道这I<B> = I<A>是合法的.

再次:方差是关于在类型转换中保持赋值兼容性关系的事实.它是不是一个亚型的一个实例可以被分配给它的超类型的变量的事实.

除代表之外还有哪些其他情况使用协方差/逆变?为什么?

  • 将方法组转换为代理使用返回和参数类型的协方差和逆变.这仅在返回/参数类型为引用类型时有效.

  • 通用委托和接口可以在其类型参数中标记为协变或逆变; 编译器将验证方差是否始终是类型安全的,如果不是,将禁止方差注释.这仅在类型参数是引用类型时有效.

  • 元素类型是引用类型的数组是协变的; 这不是类型安全的,但它是合法的.也就是说,你可以使用一个预期的Giraffe[]任何地方Animal[],即使你可以将一只乌龟放入一系列动物但不会放入一系列长颈鹿.尽量避免这样做.

请注意,C#不支持虚函数返回类型协方差.也就是说,您可能不会创建基类方法virtual Animal M(),然后在派生类中override Giraffe M().C++允许这样做,但C#没有.

  • @ OlivierJacot-Descombes:事实上,它已被提出十多年了.当我看到它时,我会相信它! (2认同)