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

Why does C# use contravariance (not covariance) in input parameters with delegate?

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

using System;

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

delegate void BaseDelegate(Base b);

在委托的使用中,不允许使用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;

TakeBBase 可以分配给 BaseDelegate。

    BaseDelegate b2 = TakeBBase;
    b2(new Derived());
    b2(new Base());

同样有趣的是,我们可以将 Base class 的子class 分配给委托中的 Base 类型参数。 covariance/contravariance 规则在前面的示例中似乎不起作用。

因为,如果您提供接受派生程度较低的输入参数的委托,此方法将获得一个参数值,该参数值的派生程度高于预期。这行得通。

另一方面,如果使用了协变,您可能会提供一个期望派生程度更高的类型的委托,但它可能会获得派生程度较低的类型的值。这不起作用。

BaseDelegate b = TakeBBase; // Contravariant. OK.
b(new Base());

因为 b 静态声明为 BaseDelegate 它接受类型 Base 的值或从它派生的类型。现在,因为 b 实际上是在调用 TakeBBase,它会在需要 BBase 值的地方传递此 Base 值。因为Base是从BBase推导出来的,所以没问题。

BaseDelegate b = TakeDerived; // Covariant. DOES NOT COMPILE!
b(new Base());

现在正在调用 TakeDerived 并正在获取类型 Base 的值,但期望类型为 Derived,而 Base 显然不是。因此协变不是类型安全的。

注意:对于输出参数,注意事项正好相反。因此 out 参数和 return 值是协变的。

有点违反直觉的是,我们不仅仅是在谈论一个或多或少派生的值,而是关于一个委托接受(或 returning)一个或多或少派生的值派生较少。

相应的参数适用于泛型类型参数。在这里,您或多或少地提供了具有方法的派生类型,对于这些方法(包括 属性 getter 和 setter),它与您的委托有相同的问题。

Olivier 的回答是正确的;我想我可能会尝试更直观地解释这一点。

Why does C# choose to use contravariance (not covariance) in input parameters in delegate?

因为逆变是类型安全的,而协变不是。

假设不是 Base,而是哺乳动物:

delegate void MammalDelegate(Mammal m);

这意味着“一个接受哺乳动物并且return什么都没有的函数”。

所以,假设我们有

void M(Giraffe x)

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

void N(Animal x)

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

covariance/contravariance rule does not seem to work in this example.

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

再说一遍。

你有一个需要哺乳动物的方法。你可以把长颈鹿递给它。 那不是协方差。即分配兼容性。该方法有一个哺乳动物类型的形式参数。那是一个变量。你有一个长颈鹿类型的值。该值可以分配给那个变量,所以它分配兼容

如果方差不是分配兼容性,那什么是方差?让我们看一两个例子:

长颈鹿的赋值与哺乳动物类型的变量兼容。因此,长颈鹿序列 (IEnumerable<Giraffe>) 与哺乳动物类型序列变量 (IEnumerable<Mammal>) 的赋值兼容。

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

您的代表示例:

哺乳动物的赋值与动物类型的变量兼容。因此,采用动物 的 方法与采用哺乳动物 .

类型的 delegate 变量的赋值兼容

逆变。再次是逆变,我们可以从其他两种类型的赋值兼容性中推断出两种事物的赋值兼容性——在这种情况下,一个方法可以赋值给一个特定类型的变量。

协变和逆变的区别只是“方向”对调了而已。通过协方差,我们知道 A can be used as B 意味着 I<A> can be used as I<B>。通过逆变我们知道 I<B> can be used as I<A>.

再次:方差是关于在类型转换中保持分配兼容性关系的事实不是可以将子类型的实例分配给其超类型的变量的事实。

What other cases than delegate use covariance/contravariance and why?

  • 方法组到委托的转换在 return 和参数类型上使用协变和逆变。这仅在 return / 参数类型是引用类型时有效。

  • 泛型委托和接口可以在它们的类型参数中标记为协变或逆变;编译器将验证变体始终是类型安全的,如果不是,将不允许变体注释。这仅在类型参数是引用类型时有效。

  • 元素类型为引用类型的数组是协变的;这不是类型安全的,但它是合法的。也就是说,您可以在任何需要 Animal[] 的地方使用 Giraffe[],即使您可以将乌龟放入动物数组但不能放入长颈鹿数组。尽量避免这样做。

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

关于上一段的更新: 这个答案写于 2016 年; 2020 年,C# 9 现在支持 return 类型协方差。