【发布时间】:2010-02-02 14:07:33
【问题描述】:
我无法理解协变和逆变之间的区别。
【问题讨论】:
标签: c# c#-4.0 covariance contravariance
我无法理解协变和逆变之间的区别。
【问题讨论】:
标签: c# c#-4.0 covariance contravariance
问题是“协变和逆变有什么区别?”
协变和逆变是将集合的一个成员与另一个成员相关联的映射函数的属性。更具体地说,映射可以相对于该集合上的关系是协变或逆变的。
考虑所有 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>也是如此。
如果我们在粗箭头的两边有两个东西,那么我们可以用相应的细箭头右侧的东西替换两边。
对于特定关系具有此属性的映射称为“协变映射”。这应该是有道理的:可以在需要动物序列的地方使用 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<Tiger> 转换为IEnumerable<Animal>?因为没有办法将长颈鹿输入到IEnumerable<Animal>。为什么我们可以将IComparable<Animal> 转换为IComparable<Tiger>?因为没有办法从IComparable<Animal> 中取出长颈鹿。有意义吗?
举个例子可能是最容易的——这当然是我记得它们的方式。
协方差
典型示例: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> 的逆变换了,如果你明白我的意思。它是一个“输出”,因为值可以从方法的实现传递到调用者的代码到,就像返回值一样。通常这种事情不会出现,幸运的是:)
【讨论】:
Action<T> 类型的参数仍然只在输出位置使用T”。 Action<T> 返回类型为 void,它如何使用 T 作为输出?或者这就是它的意思,因为它没有返回任何你可以看到它永远不会违反规则的东西?
我希望我的帖子有助于获得一个与语言无关的主题视图。
在我们的内部培训中,我使用了精彩的书籍“Smalltalk, Objects and Design (Chamond Liu)”,并改写了以下示例。
“一致性”是什么意思?这个想法是设计具有高度可替换类型的类型安全类型层次结构。如果您使用静态类型语言,获得这种一致性的关键是基于子类型的一致性。 (我们将在这里从高层次上讨论 Liskov 替换原则 (LSP)。)
实际例子(伪代码/C#无效):
协方差:让我们假设 Birds 用静态类型“一致地”产蛋:如果 Bird 类型产蛋,那么 Bird 的子类型不会产蛋的子类型吗?例如。 Duck 类型放置一个 DuckEgg,然后给出一致性。为什么这是一致的?因为在这样的表达式中:Egg anEgg = aBird.Lay();引用 aBird 可以合法地被 Bird 或 Duck 实例替换。我们说返回类型与定义 Lay() 的类型是协变的。子类型的覆盖可能会返回更特殊的类型。 => “他们提供更多。”
逆变:假设钢琴家可以通过静态类型“一致地”演奏钢琴:如果钢琴家演奏钢琴,她是否能够演奏三角钢琴?不是更喜欢演奏家演奏三角钢琴吗? (警告;有一个转折!)这是不一致的!因为在这样的表达式中:aPiano.Play(aPianist); aPiano 不能被 Piano 或 GrandPiano 实例合法地替代! GrandPiano 只能由 Virtuoso 演奏,钢琴家太笼统了! GrandPianos 必须能被更一般的类型演奏,然后演奏是一致的。我们说参数类型与定义 Play() 的类型是逆变的。子类型的覆盖可以接受更通用的类型。 => “他们需要的更少。”
返回 C#:
因为 C# 基本上是一种静态类型语言,所以类型接口的“位置”应该是协变或逆变的(例如参数和返回类型),必须显式标记以保证该类型的一致使用/开发,以使LSP 工作正常。在动态类型语言中,LSP 一致性通常不是问题,换句话说,如果您只在类型中使用动态类型,您可以完全摆脱 .Net 接口和委托上的协变和逆变“标记”。 - 但这不是 C# 中的最佳解决方案(您不应该在公共接口中使用动态)。
回到理论:
所描述的一致性(协变返回类型/逆变参数类型)是理论上的理想(由 Emerald 和 POOL-1 语言支持)。一些 oop 语言(例如 Eiffel)决定应用另一种类型的一致性,尤其是。也是协变参数类型,因为它比理论理想更能描述现实。
在静态类型语言中,所需的一致性通常必须通过应用“双重调度”和“访问者”等设计模式来实现。其他语言提供所谓的“多重调度”或多重方法(这基本上是在运行时选择函数重载,例如使用 CLOS)或通过使用动态类型来获得所需的效果。
【讨论】:
Bird 定义了public abstract BirdEgg Lay();,那么Duck : Bird 必须 实现public override BirdEgg Lay(){} 所以你断言BirdEgg anEgg = aBird.Lay(); 有任何类型的差异是完全不正确的。作为解释点的前提,现在整个点都没有了。您会相反说协方差存在于 DuckEgg 隐式转换为 BirdEgg 输出/返回类型的实现中吗?无论哪种方式,请消除我的困惑。
DuckEgg Lay() 不是 Egg Lay() 在 C# 中 的有效覆盖,这就是症结所在。 C# 不支持协变返回类型,但 Java 和 C++ 都支持。我宁愿使用类似 C# 的语法来描述理论理想。在 C# 中,您需要让 Bird 和 Duck 实现一个通用接口,其中 Lay 被定义为具有协变返回(即超出规范)类型,然后事情就凑合了!
extends, Consumer super”。
Co 和 Contra 方差是非常合乎逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过例子很容易理解。
例如,您想买一朵花,而您所在的城市有两家花店:玫瑰店和雏菊店。
如果你问某人“花店在哪里?”有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是一朵花,如果你想买一朵花,你可以买一朵玫瑰。如果有人用雏菊店的地址回复您,这同样适用。
这是协方差的示例:您可以将A<C> 转换为A<B>,其中C 是B 的子类,如果A 产生通用值(返回作为函数的结果)。协方差是关于生产者的,这就是为什么 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<B> 转换为A<C>,其中C 是B 的子类,如果A 使用通用值.逆变是关于消费者的,这就是为什么 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());
【讨论】:
转换器委托帮助我理解差异。
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 是同一把椅子的店主。
逆变。现在我们不能将 Bob 命名为家具店主,因为他不会把桌子带到他的商店,他只会存放椅子。但是我们可以称他为紫色椅子的店主,因为紫色的椅子就是椅子。这是IBookkeeper<in T>,我们允许分配给更具体的类型,而不是更少。 in 代表数据流入对象。
协方差。相反,我们可以将 Alice 命名为家具柜台,因为它不会影响她的角色。但是我们不能将她命名为红色椅子的柜台,因为我们希望她不会计算非红色椅子,但她会计算它们。这是ICounter<out T>,允许隐式转换为不太具体,而不是更具体。 out 代表数据流出对象。
不变性是我们不能两者兼得的时候。
【讨论】: