逆向解释

时间:2009-12-26 04:48:18

标签: c# c#-4.0 covariance contravariance

首先,我已经阅读了关于协方差和逆变的SO和博客的许多解释,并且非常感谢Eric LippertCovariance and Contravariance上制作如此精彩的系列节目。

然而,我有一个更具体的问题,我正试图让我的头脑稍微偏执。

据我所知Eric's explanation,协方差和反演是描述转换的形容词。协变变换是保留类型顺序的变换,逆变变换是反转变换的变换。

我理解协方差,我认为大多数开发人员直观地理解。

//covariant operation
Animal someAnimal = new Giraffe(); 
//assume returns Mammal, also covariant operation
someAnimal = Mammal.GetSomeMammal(); 

这里的返回操作是协变的,因为我们保留了动物仍然比哺乳动物或长颈鹿大的大小。在这方面,大多数返回操作都是协变的,逆变操作是没有意义的。

  //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(); 

这段代码当然对大多数开发人员没有意义。

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

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 个答案:

答案 0 :(得分:27)

更新:哎呀。事实证明,我在最初的答案中混淆了方差和“赋值兼容性”。相应地编辑了答案。我还写了一篇博文,希望能更好地回答这些问题: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#中,数组,委托和通用接口支持variance。正如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());
    
  2. Using Variance in Delegates

    // 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;
     }
    
  3. Using Variance for Func and Action Generic Delegates

     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;
    
  4. 希望这有帮助。

答案 1 :(得分:13)

协方差和反演法不是在实例化课程时可以观察到的事情。因此,在查看简单的类实例化时,谈论其中一个是错误的,就像在您的示例中一样: Animal someAnimal = new Giraffe(); //covariant operation

这些条款不对操作进行分类。 术语Covariance,Contravariance和Invariance描述了类及其子类的某些方面之间的关系。

协方差
表示方面更改类似于继承方向。
逆变
表示方面与继承方向相反。
不变性
表示方面不会从类更改为其子类。

在谈论Cov。时,我们通常会考虑以下几个方面。和Inv。:

  • 方法
    • 参数类型
    • 返回类型
    • 其他与签名相关的方面,例如抛出异常。
  • 泛型

让我们看几个例子来更好地理解这些术语

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) }
在这两种情况下,“方法”都被覆盖了!此外,上述示例是Cov的唯一合法出现。和反对。面向对象语言。:

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

让我们看一些反例来更好地理解上面的列表:

//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).

这个话题非常复杂,我可以继续这么长时间。我建议你检查一下Cov。和反对。你自己的泛型。此外,您需要知道动态绑定如何工作以完全理解示例(确切地调用哪些方法)。

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

答案 2 :(得分:10)

我的理解是,不是共型/反型变体的子类型关系,而是这些类型(例如委托和泛型)之间的操作(或投影)。因此:

Animal someAnimal = new Giraffe();

不是共变体,而是仅仅是赋值兼容性,因为Giraffe类型比“Animal”类型“小”。当您在这些类型之间进行一些投影时,Co / contra-variance会成为一个问题,例如:

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

这在C#3中无效,但它应该是可能的,因为一系列长颈鹿是一系列动物。投影T -> IEnumerable<T>保留了自Giraffe < AnimalIEnumerable<Giraffe> < IEnumerable<Animal>以来类型关系的“方向”(请注意,分配要求左侧的类型至少与右侧一样宽) )。

Contra-variance扭转了类型关系:

Action<Animal> printAnimal = a => {System.Console.WriteLine(a.Name)};
Action<Giraffe> printGiraffe = printAnimal;

这在C#3中也是不合法的,但它应该是因为任何采取动物的动作都可以应对通过长颈鹿。但是,由于Giraffe < AnimalAction<Animal> < Action<Giraffe>投影已经颠倒了类型关系。这在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());

答案 3 :(得分:1)

(编辑回应评论)

This MSDN article on the topic描述了协方差和逆变,因为它适用于将函数与委托匹配。委托类型的变量:

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

可以(由于逆转)填充函数:

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

从我的阅读中,它与直接调用函数无关,而是与代理匹配函数。我不确定它是否可以归结为您演示的级别,个别变量或对象分配是逆变或协变的。但是,根据链接的文章,委托人的分配使用了对我有意义的方式的逆变或协方差。因为委托的签名包含比实际实例更多的派生类型,所以这被称为“逆变”,与“协方差”分开,其中委托的返回类型的派生类型少于实际实例。

答案 4 :(得分:1)

以这种方式看待它: 如果我有一个处理子类型哺乳动物的函数func,其形式为 Mammal m = Func(g(哺乳动物)),我可以将Mammal换成包含 < em>哺乳动物,这里是Base Animal

就体育类比来理解下面的图像,你可以像在板球中一样用赤手抓球,但也可以(并且更容易)使用棒球手套接球。

你在左边看到的是协方差,你在参数部分看到的是逆变。

enter image description here

您可能想知道&#34;为什么左绿色曲线比红色曲线大?是不是通常比基本类型更大的子类型?&#34; 答案:不可以。支架的大小表示允许的各种物体,如维恩图。一套哺乳动物比Set Animal小。类似地,f(哺乳动物)小于f(动物),因为它仅支持较小的一组对象。 (即处理哺乳动物的功能不会处理所有动物,但处理动物的功能总能处理哺乳动物)。因此,这种关系被颠倒了,因为f(动物)可以传递而不是f(哺乳动物),从而使其逆变。