协方差与反方差的区别

我很难理解反变之间的区别。

34815 次浏览

举例子可能是最容易的——我当然是这样记住他们的。

协方差

规范示例: 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>转换为 IComparer<string>,或者将 Action<object>转换为 Action<string>; 这些对象的值只能转换为 进入

这一次它起作用了,因为如果 API 期望得到一些通用的东西(比如 object) ,那么您可以给它一些更具体的东西(比如 string)。

一般来说

如果您有一个接口 IFoo<T>,它可以在 T中协变(即,如果 T仅用于接口内的输出位置(例如返回类型) ,则将其声明为 IFoo<out T>。如果 T只用于输入位置(例如参数类型) ,它在 T(即 IFoo<in T>)中可能是逆变的。

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

问题是“反变之间的区别是什么?”

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

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

{ Animal,
Tiger,
Fruit,
Banana }.

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

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

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

到目前为止?

现在让我们考虑一个 关系。在第一个集合中的类型对之间有一个 赋值兼容关系赋值兼容关系Tiger类型的值可以赋给 Animal类型的变量,因此这些类型被称为“赋值兼容”。让我们用更短的形式写下“一个 X类型的值可以赋给一个 Y类型的变量”: X ⇒ 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>也是对的。

如果我们在一个胖箭头的两边都有两个东西,那么我们可以用相应的细箭头右边的东西来代替两边。

对于特定关系具有此属性的映射称为“协变映射”。这应该是有意义的: 老虎序列可以用在需要动物序列的地方,但反过来就不行了。在需要老虎序列的地方,不一定要使用动物序列。

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

{ 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中的反变之间的区别。协方差 abc 0可分配性的方向。逆方差 abc 1它。

我希望我的文章有助于获得一个语言不可知论的观点的主题。

对于我们的内部培训,我已经与精彩的书“ Smalltalk,对象和设计(刘夏蒙)”,我重新措辞以下的例子。

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

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

  • 协方差: 让我们假设鸟类产蛋“一致”与静态类型: 如果类型鸟下一个蛋,不会鸟的子类型下一个子类型的蛋?例如鸭子下鸭蛋的类型,然后给出稠度。为什么这是一致的?因为在这样的表达式中: Egg anEgg = aBird.Lay();引用 aBird 可以被 Bird 或 Duck 实例合法地替换。我们说返回类型与定义了 Lay ()的类型是协变的。子类型的重写可能返回一个更专门化的类型。= > “他们提供更多。”

  • 矛盾: 让我们假设钢琴家可以用静态打字“一致地”演奏钢琴: 如果钢琴家演奏钢琴,她能够演奏大钢琴吗?难道一个大师不想弹一架大钢琴吗?(请注意,事情有点反常!)这是前后矛盾的!因为在这样的表达中: aPiano.Play(aPianist);aPiano 不能合法地被钢琴或 GrandPiano 实例所替代!大钢琴只能由大师演奏,钢琴家太一般了!大钢琴必须可以由更一般的类型发挥,然后发挥是一致的。我们说参数类型与定义 Play ()的类型相反。子类型的重写可以接受更广义的类型。= > “他们要求更少。”

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

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

转换器委托有助于我理解其中的区别。

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

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

协方差

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

如果你问某人“花店在哪里?”然后有人告诉你玫瑰花店在哪里,可以吗?是的,因为玫瑰是一种花,如果你想买一朵花,你可以买一朵玫瑰。如果有人回复你菊花店的地址,也是一样。

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

类型:

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使用通用值,则允许将 A<B>强制转换为 A<C>,其中 CB的子类。反差是关于消费者的,这就是为什么 C # 使用关键字 in作为反差。

类型:

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

连结

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

违规行为。现在我们不能给鲍勃指定一个家具店老板,因为他不会把桌子带到他的店里,他只存放椅子。但是我们可以给他起名叫紫色椅子的店主,因为紫色的就是椅子。这是 IBookkeeper<in T>,我们允许分配到更具体的类型,而不是更少。in代表进入对象的数据流。

Covarinace.恰恰相反,我们可以给爱丽丝取名为家具柜台,因为这不会影响她的角色。但是我们不能给她取一个红色椅子的名字,因为我们希望她不去数非红色的椅子,但是她会去数它们。这是 ICounter<out T>,允许隐式转换为不太具体,而不是更具体。out表示从对象中流出的数据流。

不变性就是我们不能两者兼顾的时候。