反变(Contravariance)解释

43
首先,我已经阅读了许多关于协变和逆变的解释,感谢Eric Lippert制作了如此出色的协变和逆变系列文章。然而,我有一个更具体的问题需要理解。据Eric的解释,协变和逆变都是描述转换的形容词。协变转换是保留类型顺序的转换,而逆变转换则是颠倒它。我理解协变的方式是,我认为大多数开发人员都可以直观地理解。
//covariant operation
Animal someAnimal = new Giraffe(); 
//assume returns Mammal, also covariant operation
someAnimal = Mammal.GetSomeMammal(); 

这里的返回操作是协变的,因为我们保留了Animal比Mammal或Giraffe大的尺寸。需要注意的是,大多数返回操作都是协变的,而逆变操作则没有意义。
  //if return operations were contravariant
  //the following would be illegal
  //as Mammal would need to be stored in something
  //equal to or less derived than Mammal
  //which would mean that Animal is now less than or equal than Mammal
  //therefore reversing the relationship
  Animal someAnimal =  Mammal.GetSomeMammal(); 

当然,对于大多数开发者来说,这段代码毫无意义。

我的困惑在于逆变参数。如果你有一个方法,例如

bool Compare(Mammal mammal1, Mammal mammal2);

我一直认为输入参数总是强制反变行为。因此,如果类型用作输入参数,则其行为应该是反变的。

然而,以下代码有什么区别

Mammal mammal1 = new Giraffe(); //covariant
Mammal mammal2 = new Dolphin(); //covariant

Compare(mammal1, mammal2); //covariant or contravariant?
//or
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant?

同样的道理,你不能做这样的事情,也不能做

   //not valid
   Mammal mammal1 = new Animal();
   
   //not valid
   Compare(new Animal(), new Dolphin());

我想问的是,什么使得方法参数传递成为一个反变换。抱歉发帖很长,也许我对此的理解不正确。
编辑:根据下面的交谈,我理解例如使用委托层可以清楚地显示反变性。考虑以下示例。
//legal, covariance
Mammal someMammal = new Mammal();
Animal someAnimal = someMammal;

// legal in C# 4.0, covariance (because defined in Interface)
IEnumerable<Mammal> mammalList = Enumerable.Empty<Mammal>();
IEnumerable<Animal> animalList = mammalList;

//because of this, one would assume
//that the following line is legal as well

void ProcessMammal(Mammal someMammal);

Action<Mammal> processMethod = ProcessMammal;
Action<Animal> someAction = processMethod;

当然,这是非法的,因为有人可以将任何动物传递给someAction,而ProcessMammal只期望任何是哺乳动物或更具体(小于哺乳动物)的东西。这就是为什么someAction必须只是Action或任何更具体的东西(Action)。
然而,这引入了一个代理层,是否有必要在中间使用代理才能进行逆变投影?如果我们将Process定义为一个接口,我们将声明参数作为逆变类型,只是因为我们不希望某人能够像上面使用委托那样做?
public interface IProcess<out T>
{
    void Process(T val);
}
5个回答

31

更新: 哎呀,事实证明,在我最初的回答中混淆了方差和“分配兼容性”。已相应地编辑了回答。我还写了一篇博客文章,希望能更好地回答这类问题: Covariance and Contravariance FAQ

答案: 我想你第一个问题的答案是,在这个例子中你没有逆变。

bool Compare(Mammal mammal1, Mammal mammal2); 
Mammal mammal1 = new Giraffe(); //covariant - no             
Mammal mammal2 = new Dolphin(); //covariant - no            

Compare(mammal1, mammal2); //covariant or contravariant? - neither            
//or             
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant? - neither
此外,这里甚至没有协变性。你拥有的是所谓的“赋值兼容性”,这意味着你总是可以将一个更派生类型的实例分配给一个更少派生类型的实例。
在C#中,数组、委托和泛型接口支持变异。正如Eric Lippert在他的博客文章What's the difference between covariance and assignment compatibility? 中所说的那样,最好将变异视为类型的“投影”。
协变性更容易理解,因为它遵循赋值兼容性规则(更派生类型的数组可分配给较不派生的数组,例如“object[] objs = new string[10];”)。逆变性颠倒了这些规则。例如,想象一下你能够做类似于“string[] strings = new object[10];” 的事情。当然,由于明显的原因,你不能这样做。但那就是逆变性(但同样,数组并不支持逆变性,它们只支持协变性)。
以下是我希望能向您展示逆变性真正含义的MSDN示例(我现在拥有这些文档,因此如果您认为文档中有什么不清楚的地方,请随时给我反馈):
1. Using Variance in Interfaces for Generic Collections
Employee[] employees = new Employee[3];
// You can pass PersonComparer, 
// which implements IEqualityComparer<Person>,
// although the method expects IEqualityComparer<Employee>.
IEnumerable<Employee> noduplicates =
    employees.Distinct<Employee>(new PersonComparer());
  • 使用委托变量中的协变性和逆变性

  • // Event hander that accepts a parameter of the EventArgs type.
    private void MultiHandler(object sender, System.EventArgs e)
    {
       label1.Text = System.DateTime.Now.ToString();
    }
    public Form1()
    {
        InitializeComponent();
        // You can use a method that has an EventArgs parameter,
        // although the event expects the KeyEventArgs parameter.
        this.button1.KeyDown += this.MultiHandler;
        // You can use the same method 
        // for an event that expects the MouseEventArgs parameter.
        this.button1.MouseClick += this.MultiHandler;
     }
    
  • 使用方差为Func和Action通用委托添加泛型

  •  static void AddToContacts(Person person)
     {
       // This method adds a Person object
       // to a contact list.
     }
    
     // The Action delegate expects 
     // a method that has an Employee parameter,
     // but you can assign it a method that has a Person parameter
     // because Employee derives from Person.
     Action<Employee> addEmployeeToContacts = AddToContacts;
    

    希望这可以帮到您。


    @Alexandra。谢谢你。你最后的例子最能说明问题。如果是反过来,一个以Employee为参数的方法,你就不能将它分配给Action<Person>。这会违反逆变参数规则。所以我猜这就是针对泛型和接口类型的“逆变”行为。 - Stan R.

    18
    协变性和逆变性不是在实例化类时可以观察到的东西。因此,在查看简单的类实例化时,比如您的例子中: Animal someAnimal = new Giraffe(); //协变操作 提到其中之一是错误的。
    这些术语并不分类操作。协变性、逆变性和不变性这些术语描述了类及其子类之间的某些方面之间的关系。
    协变性意味着某个方面的变化与继承方向类似。
    逆变性意味着某个方面的变化与继承方向相反。
    不变性意味着某个方面在从一个类到其子类中不会发生改变。
    当谈论协变性、逆变性和不变性时,我们通常考虑以下方面:
    方法 参数类型 返回类型 其他与签名相关的方面,如抛出的异常。
    泛型

    Let us have a look at a few examples to get a better understanding of the terms.

    class T
    class T2 extends T
     
    //Covariance: The return types of the method "method" have the same
    //direction of inheritance as the classes A and B.
    class A { T method() }
    class B extends A { T2 method() }
     
    //Contravariance: The parameter types of the method "method" have a
    //direction of inheritance opposite to the one of the classes A and B.
    class A { method(T2 t) }
    class B { method(T t) }
    In both cases, "method" gets overridden! Further, the above examples are the only legal occurrences of Cov. and Contrav. in object oriented languages.:

    • 协变性 - 返回类型和异常抛出语句
    • 逆变性 - 输入参数
    • 不变性 - 输入和输出参数

    Let us have a look at some counter examples to better understand the above list:

    //Covariance of return types: OK
    class Monkey { Monkey clone() }
    class Human extends Monkey { Human clone() }
     
    Monkey m = new Human();
    Monkey m2 = m.clone(); //You get a Human instance, which is ok,
                           //since a Human is-a Monkey.
     
    //Contravariance of return types: NOT OK
    class Fruit
    class Orange extends Fruit
     
    class KitchenRobot { Orange make() }
    class Mixer extends KitchenRobot { Fruit make() }
     
    KitchenRobot kr = new Mixer();
    Orange o = kr.make(); //Orange expected, but got a fruit (too general!)
     
    //Contravariance of parameter types: OK
    class Food
    class FastFood extends Food
     
    class Person { eat(FastFood food) }
    class FatPerson extends Person { eat(Food food) }
     
    Person p = new FatPerson();
    p.eat(new FastFood()); //No problem: FastFood is-a Food, which FatPerson eats.
     
    //Covariance of parameter types: NOT OK
    class Person { eat(Food food) }
    class FatPerson extends Person { eat(FastFood food) }
     
    Person p = new FatPerson();
    p.eat(new Food()); //Oops! FastFood expected, but got Food (too general).

    这个话题非常复杂,我可以讲很长时间。我建议您自己检查泛型的协变和逆变。此外,您需要了解动态绑定是如何工作的,才能完全理解示例(哪些方法被调用)。

    这些术语源于Liskov替换原则,该原则定义了将数据类型建模为另一个数据类型的子类型所必需的标准。您可能还想进行调查。


    1
    你太厉害了。没有人能比你更好地向我解释这些主题。 - Navjot Singh

    10

    我的理解是,协变和逆变不是子类型关系,而是在这些类型之间的操作(或投影)中发生的(例如委托和泛型)。因此:

    Animal someAnimal = new Giraffe();
    

    不是协变的,而只是赋值兼容性,因为Giraffe类型比Animal类型“更小”。当这些类型之间存在某种投影时,例如:

    IEnumerable<Giraffe> giraffes = new[] { new Giraffe() };
    IEnumerable<Animal> animals = giraffes;
    

    这在C#3中是无效的,但应该是可能的,因为长颈鹿序列是动物序列。投影 T -> IEnumerable<T> 保留了类型关系的“方向”,因为 Giraffe < Animal 并且 IEnumerable<Giraffe> < IEnumerable<Animal>(请注意,赋值要求左侧的类型至少与右侧一样宽)。
    逆变性反转了类型关系:
    Action<Animal> printAnimal = a => {System.Console.WriteLine(a.Name)};
    Action<Giraffe> printGiraffe = printAnimal;
    

    这在C#3中也是不合法的,但实际上应该是合法的,因为任何针对动物的操作都可以处理传递给它的长颈鹿。然而,由于 长颈鹿 < 动物Action<动物> < Action<长颈鹿> ,所以类型关系被颠倒了。这在C#4中是合法的。

    所以回答你例子中的问题:

    //the following are neither covariant or contravariant - since there is no projection this is just assignment compatibility
    Mammal mammal1 = new Giraffe();
    Mammal mammal2 = new Dolphin();
    
    //compare is contravariant with respect to its arguments - 
    //the delegate assignment is legal in C#4 but not in C#3
    Func<Mammal, Mammal, bool> compare = (m1, m2) => //whatever
    Func<Giraffe, Dolphin, bool> c2 = compare;
    
    //always invalid - right hand side must be smaller or equal to left hand side
    Mammal mammal1 = new Animal();
    
    //not valid for same reason - animal cannot be assigned to Mammal
    Compare(new Animal(), new Dolphin());
    

    李,我当然了解赋值兼容性与协变/逆变性之间的区别。引入中间层,例如委托,当然可以非常清楚地解释该问题。人们会认为Action<Mammal>可以分配给Action<Animal>,但这当然是错误的,只有Action<Mammal>或更少的内容可以分配给Action<Mammal>。是的,这种中间层明显显示出逆变性,但只有因为有一层..我们是否在说只有在引入此类层时才会发生逆变性。 - Stan R.
    我本来想说协变/逆变... :),无论如何我已经编辑了我的问题。我确信赋值兼容性是协变的,因为协变保留了赋值兼容性。然而,在Action<Animal> = Action<Mammal>这个例子中,这种赋值兼容性并没有被保留,而是被反转了...这就是逆变。尽管Action<Animal> = Action<Mammal>仍然是赋值兼容的(但只有因为声明它为逆变才是)。 - Stan R.

    3
    这样看待它: 如果我有一个处理哺乳动物子类型的函数func,形式为Mammal m = Func(g(Mammal)),我可以用包含Mammal的东西替换掉Mammal,这里是基本的Animal
    就体育类比来理解下面的图像,你可以像板球那样用裸手接球,但也可以(更容易)使用棒球手套接球。
    左边所见的是协变性,参数部分内部所见的是逆变性。

    enter image description here

    你可能会想:“为什么左边的绿色曲线比红色曲线大?通常做得比基本类型多的子类型不应该更大吗?” 回答:不是这样的。括号的大小表示允许的对象种类,就像维恩图一样。哺乳动物集合比动物集合小。同样,f(哺乳动物)比f(动物)小,因为它只支持一个较小的对象集合。(即处理哺乳动物的函数无法处理所有动物,但处理动物的函数总是能处理哺乳动物)。因此,关系被反转,因为f(动物)可以代替f(哺乳动物)传入,从而使其逆变。

    +1,这个问题已经快7年了,我当时已经解决了,但对于其他人来说可能会有所帮助。 - Stan R.

    1

    (根据评论编辑)

    这篇MSDN文章描述了协变性和逆变性在将函数与委托匹配时的应用。委托类型的变量:

    public delegate bool Compare(Giraffe giraffe, Dolphin dolphin);
    

    因为逆变的缘故,可以用以下函数填充:

    public bool Compare(Mammal mammal1, Mammal mammal2)
    {
        return String.Compare(mammal1.Name, mammal2.Name) == 0;
    }
    

    从我的阅读来看,它与直接调用函数无关,而是与将函数与委托匹配有关。我不确定它是否可以简化为您展示的级别,即使用单个变量或对象分配进行逆变或协变。但是,根据链接的文章,委托的赋值使用逆变或协变方式对我来说是有意义的。因为委托的签名包含比实际实例更派生的类型,所以这被称为“逆变”,这与“协变”是不同的,在协变中,委托的返回类型比实际实例不太派生。

    这绝对不是它适用的唯一领域。你文章中讨论的只是这个,因为标题是“委托中的协变和逆变”。它并没有像一个名为“灵长类动物交配”的东西不会谈论鲑鱼产卵那样提到其他任何事情;这并不意味着它们不存在。 - Adam Robinson
    无论他对这个主题的理解是否正确,这仍然是一篇有用的文章和评论。 - Joseph Yaduvanshi
    @Jim:这是一个有用的评论,但“协变和逆变仅适用于将函数与委托匹配”是错误的,并且只会通过误导增加混乱,使这个已经非常广泛被误解的主题更加复杂。 - Adam Robinson
    2
    你所参考的文章来自.NET 2.0文档。当时,委托中只存在变异性。如果你查看这篇文章的.NET 4.0版本 (http://msdn.microsoft.com/en-us/library/ms173174(VS.100).aspx),你会发现它现在是有关协变性和逆变性的更大文档集的一部分。 - Alexandra Rusina

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接