我无法理解协方差和逆变之间的区别。
6 回答
问题是“协变和逆变之间有什么区别?”
协变和逆变是映射函数的属性,它将集合的一个成员与另一个成员相关联。更具体地说,映射可以相对于该集合上的关系是协变或逆变的。
考虑所有 C# 类型集的以下两个子集。第一的:
{ Animal,
Tiger,
Fruit,
Banana }.
其次,这个明显相关的集合:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
从第一组到第二组有一个映射操作。也就是说,对于第一组中的每个 T,第二组中对应的类型是IEnumerable<T>
。或者,简而言之,映射是T → IE<T>
. 请注意,这是一个“细箭头”。
跟我到现在?
现在让我们考虑一个关系。第一组类型对之间存在赋值兼容关系。type 的值Tiger
可以分配给 type 的变量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>
。
如果我们在粗箭头的两边有两个东西,那么我们可以用相应的细箭头右侧的东西来替换两边。
对于特定关系具有此属性的映射称为“协变映射”。这应该是有道理的:可以在需要动物序列的地方使用 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 中协变和逆变之间的区别。协变保留了可分配性的方向。逆变将它反转。
举个例子可能是最容易的——这当然是我记得它们的方式。
协方差
典型例子: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>
如果你明白我的意思的话,它的逆变性。这是一个“输出”,因为值可以从方法的实现传递到调用者的代码,就像返回值一样。通常这种事情不会出现,幸运的是:)
我希望我的帖子有助于获得一个与语言无关的主题视图。
在我们的内部培训中,我使用了精彩的书籍“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)或通过使用动态类型来获得所需的效果。
Co和Contra方差是非常合乎逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过示例很容易理解。
协方差
例如,你想买一朵花,你所在的城市有两家花店:玫瑰店和雏菊店。
如果你问别人“花店在哪里?” 有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是一朵花,如果你想买一朵花,你可以买一朵玫瑰。如果有人用雏菊店的地址回复您,这同样适用。
这是协方差的示例:如果产生泛型值(作为函数的结果返回) ,则允许您强制A<C>
转换为A<B>
,其中C
是 的子类。协方差是关于生产者的,这就是为什么 C# 使用关键字来表示协方差。B
A
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<B>
为A<C>
,其中C
是 的子类。逆变是关于消费者的,这就是 C# 使用关键字进行逆变的原因。B
A
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());
链接
转换器代表帮助我理解差异。
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();
考虑一个组织中有两个职位。爱丽丝是椅子的柜台。鲍勃是同一把椅子的店主。
逆变。现在我们不能将 Bob 命名为家具店主,因为他不会把桌子带到他的商店,他只会存放椅子。但我们可以称他为紫色椅子的店主,因为紫色的椅子就是椅子。也就是说IBookkeeper<in T>
,我们允许分配给更具体的类型,而不是更少。in
代表数据流入对象。
协方差。相反,我们可以将 Alice 命名为家具柜台,因为它不会影响她的角色。但是我们不能将她命名为红色椅子的柜台,因为我们希望她不会计算非红色椅子,但她会计算它们。也就是说ICounter<out T>
,允许隐式转换为不太具体,而不是更具体。out
代表数据流出对象。
不变性是我们不能两者兼得。