167

我无法理解协方差和逆变之间的区别。

4

6 回答 6

282

问题是“协变和逆变之间有什么区别?”

协变和逆变是映射函数的属性,它将集合的一个成员与另一个成员相关联。更具体地说,映射可以相对于该集合上的关系是协变或逆变的。

考虑所有 C# 类型集的以下两个子集。第一的:

{ Animal, 
  Tiger, 
  Fruit, 
  Banana }.

其次,这个明显相关的集合:

{ IEnumerable<Animal>, 
  IEnumerable<Tiger>, 
  IEnumerable<Fruit>, 
  IEnumerable<Banana> }

从第一组到第二组有一个映射操作。也就是说,对于第一组中的每个 T,第二组中对应的类型是IEnumerable<T>。或者,简而言之,映射是T → IE<T>. 请注意,这是一个“细箭头”。

跟我到现在?

现在让我们考虑一个关系。第一组类型对之间存在赋值兼容关系。type 的值Tiger可以分配给 type 的变量Animal,因此这些类型被称为“赋值兼容”。让我们用更短的形式写“一个类型的值X可以分配给一个类型的变量”: 。请注意,这是一个“胖箭头”。YX ⇒ Y

所以在我们的第一个子集中,这里是所有的赋值兼容性关系:

Tiger  ⇒ Tiger
Tiger  ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit  ⇒ Fruit

在支持某些接口的协变赋值兼容性的 C# 4 中,第二组类型对之间存在赋值兼容性关系:

IE<Tiger>  ⇒ IE<Tiger>
IE<Tiger>  ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit>  ⇒ IE<Fruit>

请注意,映射T → IE<T> 保留了赋值兼容性的存在和方向。也就是说,如果X ⇒ Y,那么 也是如此IE<X> ⇒ IE<Y>

如果我们在粗箭头的两边有两个东西,那么我们可以用相应的细箭头右侧的东西来替换两边。

对于特定关系具有此属性的映射称为“协变映射”。这应该是有道理的:可以在需要动物序列的地方使用 Tigers 序列,但反之则不然。在需要老虎序列的情况下,不一定要使用动物序列。

这就是协方差。现在考虑所有类型集合的这个子集:

{ IComparable<Tiger>, 
  IComparable<Animal>, 
  IComparable<Fruit>, 
  IComparable<Banana> }

现在我们有了从第一组到第三组的映射T → IC<T>

在 C# 4 中:

IC<Tiger>  ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger>     Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit>  ⇒ IC<Banana>     Backwards!
IC<Fruit>  ⇒ IC<Fruit>

即映射T → IC<T>保留了存在性,但反转了赋值兼容的方向。也就是说,如果X ⇒ Y,那么IC<X> ⇐ IC<Y>

保留但反转关系的映射称为逆变映射。

同样,这应该是正确的。可以比较两只动物的设备也可以比较两只老虎,但是可以比较两只老虎的设备不一定能比较任何两只动物。

这就是 C# 4 中协变和逆变之间的区别。协变保留了可分配性的方向。逆变将它反转

于 2010-02-04T18:58:32.563 回答
121

举个例子可能是最容易的——这当然是我记得它们的方式。

协方差

典型例子:IEnumerable<out T>,Func<out T>

您可以从转换IEnumerable<string>IEnumerable<object>或。值仅来自这些对象。Func<string>Func<object>

它之所以有效,是因为如果您只是从 API 中取出值,并且它会返回特定的东西(例如string),您可以将返回的值视为更通用的类型(例如object)。

逆变

典型例子:IComparer<in T>,Action<in T>

您可以从IComparer<object>toIComparer<string>Action<object>to转换为Action<string>; 值只进入这些对象。

这次它起作用了,因为如果 API 需要一些通用的东西(比如object),你可以给它一些更具体的东西(比如string)。

更普遍

如果你有一个接口IFoo<T>,它可以是协变的T(即声明它IFoo<out T>好像T只用在接口内的输出位置(例如返回类型)。它可以是逆变的T(即IFoo<in T>)如果T只用在输入位置(例如参数类型)。

它可能会令人困惑,因为“输出位置”并不像听起来那么简单——类型的参数Action<T>仍然只T在输出位置中使用——Action<T>如果你明白我的意思的话,它的逆变性。这是一个“输出”,因为值可以从方法的实现传递到调用者的代码,就像返回值一样。通常这种事情不会出现,幸运的是:)

于 2010-02-02T14:12:09.323 回答
17

我希望我的帖子有助于获得一个与语言无关的主题视图。

在我们的内部培训中,我使用了精彩的书籍“Smalltalk, Objects and Design (Chamond Liu)”,并重新表述了以下示例。

“一致性”是什么意思?这个想法是设计具有高度可替换类型的类型安全类型层次结构。如果您使用静态类型语言,获得这种一致性的关键是基于子类型的一致性。(我们将在这里从高层次讨论 Liskov 替换原则 (LSP)。)

实际示例(伪代码/在 C# 中无效):

  • 协方差:让我们假设 Birds 使用静态类型“一致地”产蛋:如果类型 Bird 产蛋,Bird 的子类型不会产蛋的子类型吗?例如 Duck 类型放置一个 DuckEgg,然后给出一致性。为什么这是一致的?因为在这样的表达式中:Egg anEgg = aBird.Lay();引用 aBird 可以合法地被 Bird 或 Duck 实例替换。我们说返回类型与定义 Lay() 的类型是协变的。子类型的覆盖可能会返回更特殊的类型。=> “他们提供更多。”</p>

  • 逆变:让我们假设钢琴家可以通过静态类型“一致地”演奏钢琴:如果钢琴家演奏钢琴,她能演奏三角钢琴吗?不是更喜欢演奏家演奏三角钢琴吗?(警告;有一个转折!)这是不一致的!因为在这样的表达方式中:aPiano.Play(aPianist);aPiano 不能被 Piano 或 GrandPiano 实例合法地替代!GrandPiano 只能由 Virtuoso 演奏,钢琴家太笼统了!GrandPianos 必须能被更一般的类型演奏,然后演奏是一致的。我们说参数类型与定义 Play() 的类型是逆变的。子类型的覆盖可以接受更通用的类型。=> “他们需要的更少。”</p>

回到 C#:
因为 C# 基本上是一种静态类型语言,所以类型接口的“位置”应该是协变或逆变的(例如参数和返回类型),必须明确标记以保证该类型的一致使用/开发,以使 LSP 正常工作。在动态类型语言中,LSP 一致性通常不是问题,换句话说,如果您只在类型中使用动态类型,您可以完全摆脱 .Net 接口和委托上的协变和逆变“标记”。- 但这不是 C# 中的最佳解决方案(您不应该在公共接口中使用动态)。

回到理论:
所描述的一致性(协变返回类型/逆变参数类型)是理论上的理想(由 Emerald 和 POOL-1 语言支持)。一些 oop 语言(例如 Eiffel)决定应用另一种类型的一致性,尤其是。也是协变参数类型,因为它比理论理想更能描述现实。在静态类型语言中,所需的一致性通常必须通过应用“双重调度”和“访问者”等设计模式来实现。其他语言提供所谓的“多重调度”或多重方法(这基本上是在运行时选择函数重载,例如使用 CLOS)或通过使用动态类型来获得所需的效果。

于 2011-07-14T20:02:14.273 回答
8

Co和Contra方差是非常合乎逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过示例很容易理解。

协方差

例如,你想买一朵花,你所在的城市有两家花店:玫瑰店和雏菊店。

如果你问别人“花店在哪里?” 有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是一朵花,如果你想买一朵花,你可以买一朵玫瑰。如果有人用雏菊店的地址回复您,这同样适用。

这是协方差的示例:如果产生泛型值(作为函数的结果返回) ,则允许您强制A<C>转换为A<B>,其中C是 的子类。协方差是关于生产者的,这就是为什么 C# 使用关键字来表示协方差。BAout

类型:

class Flower {  }
class Rose: Flower { }
class Daisy: Flower { }

interface FlowerShop<out T> where T: Flower {
    T getFlower();
}

class RoseShop: FlowerShop<Rose> {
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop: FlowerShop<Daisy> {
    public Daisy getFlower() {
        return new Daisy();
    }
}

问题是“花店在哪里?”,答案是“那里的玫瑰店”:

static FlowerShop<Flower> tellMeShopAddress() {
    return new RoseShop();
}

逆变

例如,你想送一朵花给你的女朋友,而你的女朋友喜欢任何花。你能认为她是一个喜欢玫瑰的人,还是一个喜欢雏菊的人?是的,因为如果她喜欢任何一朵花,她就会同时喜欢玫瑰和雏菊。

这是逆变的一个示例:如果使用泛型值,您可以转换A<B>A<C>,其中C是 的子类。逆变是关于消费者的,这就是 C# 使用关键字进行逆变的原因。BAin

类型:

interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
    void takeGift(TFavoriteFlower flower);
}

class AnyFlowerLover: PrettyGirl<Flower> {
    public void takeGift(Flower flower) {
        Console.WriteLine("I like all flowers!");
    }
}

你把喜欢任何花的女朋友当成喜欢玫瑰的人,给她一朵玫瑰:

PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

链接

于 2019-10-12T12:06:59.300 回答
7

转换器代表帮助我理解差异。

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput表示方法返回更具体类型的协方差

TInput表示方法传递一个不太具体的类型的逆变

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
于 2017-10-06T02:51:08.727 回答
-1

考虑一个组织中有两个职位。爱丽丝是椅子的柜台。鲍勃是同一把椅子的店主。

逆变。现在我们不能将 Bob 命名为家具店主,因为他不会把桌子带到他的商店,他只会存放椅子。但我们可以称他为紫色椅子的店主,因为紫色的椅子就是椅子。也就是说IBookkeeper<in T>,我们允许分配给更具体的类型,而不是更少。in代表数据流入对象。

协方差。相反,我们可以将 Alice 命名为家具柜台,因为它不会影响她的角色。但是我们不能将她命名为红色椅子的柜台,因为我们希望她不会计算非红色椅子,但她会计算它们。也就是说ICounter<out T>,允许隐式转换为不太具体,而不是更具体。out代表数据流出对象。

不变性是我们不能两者兼得。

于 2021-06-03T06:53:49.957 回答