7

我很难理解这两个概念。但我认为经过许多视频和 SO QA 之后,我将其提炼为最简单的形式:

协变- 假设子类型可以做它的基本类型所做的事情。
逆变- 假设您可以像对待基本类型一样对待子类型。

假设这三个类:

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(逆变),但在其核心,两者都只是允许调用基类的行为。

例如在上面的例子中,你只是考虑了一个事实,即两者CatDog子类型Animal都有方法LiveDie- 它们很自然地从它们的基类继承Animal

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

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

在逆变的情况下,我们将一个子类型传递给一个我们知道只会调用基类型行为的函数(因为基类型是形参类型),所以我们可以安全地转换基类型到一个子类型。

4

3 回答 3

8

方差- 指复杂类型(数组、列表、委托、泛型)如何与其基础类型的子类型化方向相关。

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

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

协方差是关于子类型方向的隐式转换的保留方向(Animal<-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;

逆变是隐式转换与子类型方向相反的方向(Animal->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
于 2013-10-09T05:26:00.810 回答
7

我很难理解这两个概念。

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

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

你还没有。

协方差意味着子类型可以做它的基本类型所做的事情。

不,那是里氏替换原则。

逆变意味着您可以像对待基本类型一样对待子类型。

不,这只是重新陈述你所说的协方差。

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

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

  • 逆变转换反转另一个转换的方向。

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

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

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

在 C# 中,这只是在何处以及以何种方式支持这两种类型的关系的问题?

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

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

人们经常问“我有一个方法,它需要一个动物序列,我手上有一个乌龟序列;为什么我必须将序列复制到一个新序列才能使用该方法?” 这是一个合理的要求,我们经常得到它,而且在 LINQ 使处理序列变得更容易之后,我们得到的频率也更高。这是一个普遍有用的功能,我们可以以合理的成本实现,所以我们实现了它。

于 2013-10-16T13:59:36.673 回答
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

于 2013-10-16T06:56:58.033 回答