协方差和反演 - 只是调用保证基类行为的不同机制?

时间:2013-10-09 03:59:29

标签: c# covariance contravariance

我正在努力理解这两个概念。但我认为在经过许多视频和SO QA后,我将其简化为最简单的形式:

协变 - 假设子类型可以执行其基类型的操作。
Contravariant - 假设您可以像处理其基本类型一样处理子类型。

假设这三个类:

class Animal
{
    void Live(Animal animal)
    {
        //born!
    }

    void Die(Animal animal)
    {
        //dead!
    }

}

class Cat : Animal
{

}

class Dog : Animal
{

}

协变

任何动物都可以做动物所做的事 假设子类型可以执行其基类型的操作。

Animal anAnimal = new Cat();
anAnimal.Live();
anAnimal.Die();

Animal anotherAnimal = new Dog();
anotherAnimal.Live();
anotherAnimal.Die();

反变

你可以对 动物做任何事情,你可以做任何动物。
假设您可以像处理其基本类型一样处理子类型。

Action<Animal> kill = KillTheAnimal;

Cat aCat = new Cat();
KillTheCat(kill, aCat);

Dog = new Dog();
KillTheDog(kill, aDog);

KillTheCat(Action<Cat> action, Cat aCat)
{  
    action(aCat);  
}

KillTheDog(Action<Dog> action, Dog aDog)
{  
    action(aDog);  
}

void KillTheAnimal(Animal anAnimal)
{
    anAnimal.Die();
}

这是对的吗?似乎在一天结束时,协方差和逆变允许你做的只是使用你自然期望的行为,即动物的每个类型都具有所有动物特征或者更一般地说 - 所有子类型实现其基类型的所有功能。看起来它只是允许显而易见的 - 它们只支持不同的机制,允许您以不同的方式获得继承的行为 - 一个从子类型转换为基本类型(协方差),另一个从基类型转换为子类型-type(Contravariance),但在其核心,两者都只允许调用基类的行为。

例如,在上述情况中,您只是考虑到Cat的{​​{1}}和Dog子类型都具有方法AnimalLive - 他们非常自然地从他们的基类Die继承。

在这两种情况下 - 协方差和逆变 - 我们允许调用保证的一般行为,因为我们确保在从特定基类的继承中调用行为的目标。

在Covariance 的情况下,我们隐式地将子类型转换为其基类型并调用基类型行为(如果基类型行为被覆盖,则无关紧要)子类型......关键是,我们知道它存在。)

在Contravariance 的情况下,我们采用一个子类型并将其传递给我们知道只调用基类型行为的函数(因为base-type是形式参数类型),所以我们可以安全地将基类型转换为子类型。

3 个答案:

答案 0 :(得分:8)

方差 - 指复杂类型(数组,列表,委托,泛型)与其基础类型的子类型方向的关系。

换句话说,它是关于允许隐式地转换复杂类型的方向。

两种复杂类型(委托)根据其基础类型Animal和Cat的关系示例。

协方差保留隐式转换方向的子类型方向( Animal&lt; -Cat

// Covariance based on type of return param of delegate
var catDelegate = new Func<Cat>(delegate {return null;});

// Allowed implicit casting from delegate based on Cat return param 
// to delegate based on Animal return param 
Func<Animal> animalDelegate = catDelegate;

Contravariance 是隐式转换的颠倒方向,关于子类型方向( Animal-&gt; Cat

// contravariance based on type of passed arguments of delegate
var animalDelegate = new Action<Animal>(delegate{});

// Allowed implicit casting from delegate based on Animal passed param 
// to delegate based on Cat passed param
Action<Cat> catDelegate = animalDelegate;

不变性不受支持的隐式投射(在任何方向)

通用列表是不变的

List<Animal> animals = new List<Cat>(); // error!
List<Cat> animals = new List<Animal>(); // error!

C#支持的差异示例

数组是协变的

Animal[] animals = new Cat[10]; // possible

通用IEnumerable是协变

IEnumerable<Animal> animals = new List<Cat>(); // possible

答案 1 :(得分:7)

  

我正在努力理解这两个概念。

是的,你是。很多人都这样做。

  

但我认为经过很多视频和SO QA之后,我将其简化为最简单的形式:

你还没有。

  

协方差意味着子类型可以执行其基类型的操作。

没有。这就是Liskov替代原则。

  

逆变法意味着您可以像处理基类型一样处理子类型。

没有。这只是重新陈述你所说的协方差。

协方差和逆变的真正升华是:

  • 协变转换会保留另一次转换的方向。

  • 逆变换转换了另一次转换的方向。

Dog可转换为AnimalIEnumerable<Dog>可转换为IEnumerable<Animal>。  方向被保留,因此IEnumerable<T>是协变的。 IComparable<Animal>可以转换为IComparable<Dog>,它可以反转转换的方向,因此它是逆变的。

  

我在数学上理解协方差的含义,所以我猜在compsci中它是一样的。

要明确一点:数学家使用“方差”来表示一堆不同的东西。数学和计算机科学共同的含义是范畴理论的定义。

  

在C#中,这只是支持这两种关系的地点和方式的问题?

数学上,variance告诉您关系是由映射保留还是反转。如果我们有映射T --> IEnumerable<T>并且关系“可以转换为通过身份或引用转换”,那么在C#中,如果X与Y相关,那么IE<X>IE<Y>相关。因此,映射是关于该关系的协变。

  

这些功能试图通过支持它们来实现什么?

人们经常要求“我有一种采取一系列动物的方法,我手头有一系列的乌龟;为什么我必须将序列复制到新的序列才能使用该方法?”这是一个合理的请求,我们经常得到它,并且在LINQ使得更容易使用序列之后我们更频繁地得到它。这是一个通常有用的功能,我们可以以合理的成本实现,所以我们实现了它。

答案 2 :(得分:2)

如果我们只考虑基类型和子类型以及如何调用基类型行为,我认为我们正在限制协方差和逆变的范围。违反康复和协方差的真正价值在于可以使用它们创建何种类型(由Eric lippert http://blogs.msdn.com/b/ericlippert/archive/2009/11/30/what-s-the-difference-between-covariance-and-assignment-compatibility.aspx解释的投影)。遵循常见问题应该能够清除您的疑问。 http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx