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

标签 c# delegates covariance contravariance

当我们有一个继承 BBase 的 Base 类和一个专门针对它的 Derived 类时,假设有一个需要 Base 的委托(delegate)。作为输入。

using System;

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

delegate void BaseDelegate(Base b);

在delegate的使用中,不允许使用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 类的子类分配给委托(delegate)中的 Base 类型参数也很有趣。协方差/逆变规则在前面的示例中似乎不起作用。
  • 为什么 C# 选择在委托(delegate)的输入参数中使用逆变(而不是协方差)?
  • 当协方差/逆变规则在 C# 中起作用时?除了委托(delegate)之外还有哪些其他情况使用协方差/逆变,为什么?
  • 最佳答案

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

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


    因为逆变是类型安全的,所以协变不是。
    而不是 Base,让我们说 Mammal:
    delegate void MammalDelegate(Mammal m);
    
    这意味着“一个接受哺乳动物并且不返回任何内容的函数”。
    所以,假设我们有
    void M(Giraffe x)
    
    我们可以将其用作哺乳动物代表吗?不可以。哺乳动物代表必须能够接受任何哺乳动物,但 M 不接受猫,它只接受长颈鹿。
    void N(Animal x)
    
    我们可以将其用作哺乳动物代表吗?是的。哺乳动物代表必须能够接受任何哺乳动物,而 N 确实接受任何哺乳动物。

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


    一开始就没有差异。您正在犯一个非常常见的错误,即混淆 分配兼容性协方差 .分配兼容性不是协方差。 协方差是类型系统转换保留赋值兼容性的属性 .
    让我再说一遍。
    你有一个需要哺乳动物的方法。你可以传给它一只长颈鹿。 这不是协方差 .即 分配兼容性 .该方法有一个类型为 Mammal 的形参。那是一个变量。你有一个 Giraffe 类型的值。该值可以分配给该变量,因此它是分配兼容的。
    如果不是分配兼容性,那么什么是方差?让我们看一两个例子:
    长颈鹿的赋值与哺乳动物类型的变量兼容。因此,长颈鹿序列 ( IEnumerable<Giraffe> ) 与哺乳动物序列类型变量 ( IEnumerable<Mammal> ) 的赋值兼容。
    那就是协方差 .协方差是我们可以从其他两种类型的赋值兼容性推导出两种类型的赋值兼容性的事实。我们知道长颈鹿可能会被分配给一个类型为animal 的变量;这让我们可以推断出关于其他两种类型的另一个赋值兼容性事实。
    您的委托(delegate)示例:
    哺乳动物的赋值与动物类型的变量兼容。因此,采用动物的方法与采用哺乳动物的委托(delegate)类型变量的赋值兼容。
    这就是逆变。逆变再次是,我们可以从其他两种类型的赋值兼容性推导出两个事物的赋值兼容性——在这种情况下,可以将一个方法分配给特定类型的变量。
    协方差和逆变之间的区别只是“方向”交换了。通过协方差我们知道 A can be used as B意味着 I<A> can be used as I<B> .通过逆变我们知道 I<B> can be used as I<A> .
    再次:方差是关于跨类型转换保持赋值兼容性关系的事实 .是不是 可以将子类型的实例分配给其父类(super class)型的变量的事实。

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


  • 将方法组转换为委托(delegate)使用返回和参数类型的协变和逆变。这仅在返回/参数类型是引用类型时有效。
  • 泛型委托(delegate)和接口(interface)可以在它们的类型参数中标记为协变或逆变;编译器将验证差异始终是类型安全的,如果不是,将禁止差异注释。这仅在类型参数是引用类型时才有效。
  • 元素类型为引用类型的数组是协变的;这不是类型安全的,但它是合法的。也就是说,您可以使用 Giraffe[]任何有 Animal[] 的地方可以预料,即使您可以将乌龟放入一组动物中,但不能放入一组长颈鹿中。尽量避免这样做。

  • 请注意,C# 不支持虚函数返回类型协方差。也就是说,您可能不会创建基类方法 virtual Animal M()然后在派生类 override Giraffe M() . C++ 允许这样做,但 C# 不允许。
    关于上一段的更新:这个答案写于2016年; 2020 年,C# 9 现在支持返回类型协方差。

    关于c# - 为什么 C# 在带有委托(delegate)的输入参数中使用逆变(而不是协方差)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37467882/

    相关文章:

    swift - 在 swift 中设置一个泛型类作为委托(delegate)

    c# - 何时使用或不使用 Lambda 表达式

    c# - 尝试挂接键盘时出现 "CallbackOnCollectedDelegate was detected"

    c# - 了解 C# 中的协变和逆变接口(interface)

    c# - 运行(一次)协方差计算

    c# - 如何使数据绑定(bind)控件刷新通过反射更改的属性值?

    c# - 删除数组中的最后一个元素,插入新元素

    C# 正则表达式 "+"组返回空字符串

    c++ - 覆盖虚函数返回类型不同且不是协变的

    c# - 如何使用标签进行简单的发票计算?