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

时间:2016-05-26 17:52:47

标签: c# delegates covariance contravariance

当我们有一个继承BBase的Base类和一个专门化它的Derived类时,让我们说有一个委托需要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类的子类分配给委托中的Base类型参数也很有趣。协方差/逆变规则似乎在前面的例子中不起作用。

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

2 个答案:

答案 0 :(得分:11)

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

  

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

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

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

delegate void MammalDelegate(Mammal m);

这意味着“一种能够吸引哺乳动物并且不返回任何东西的功能”。

所以,假设我们有

void M(Giraffe x)

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

void N(Animal x)

我们可以将其作为哺乳动物代表使用吗?是。哺乳动物的代表必须能够接受任何哺乳动物,而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#没有。

答案 1 :(得分:3)

因为,如果您提供一个委托接受较少派生的输入参数,则此方法将获得一个参数值,该类型的派生类型多于预期。这很有效。

另一方面,如果使用协方差,您可能会提供一个期望更多派生类型的委托,但它可能会得到一个较少派生类型的值。这不起作用。

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

由于b被静态声明为BaseDelegate,因此它接受类型Base的值或从中派生的类型。现在,由于b实际上正在调用TakeBBase,因此它会传递此Base值,其中值为BBase。由于Base源自BBase,因此可以。

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

现在正在调用TakeDerived并获得Base类型的值,但期望Derived类型之一,Base显然不是out。因此,协方差不是类型安全的。

注意:对于输出参数,考虑因素完全相反。因此(function(){ "use strict"; angular.module('moduleA').directive('whenScrolled',Directive); Directive.$inject = []; function Directive(){ return { restrict : 'A', // Restrict to Attributes link : postLink }; // Bind the element's scroll event function postLink(scope,elem,attr){ var raw = elem[0]; elem.bind('scroll',eventMethod); function eventMethod(){ if ( raw.scrollTop + raw.offsetHeight >= raw.scrollHeight ) { scope.$apply(attr.whenScrolled); } }; }; }; })(); 参数和返回值是协变的。

使它有点违反直觉的是,我们不只是讨论一个或多或少派生的值,而是关于委托接受(或返回)或多或少派生的值。

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