协方差与反方差之间的差异
我无法理解协方差和逆变之间的差异。
问题是“协变与逆变之间有什么区别?”
协变和逆变是映射函数的属性, 它将集合中的一个成员与另一个成员关联起来 。 更具体地说,映射可以是关于该集合上的关系的协变或逆变。
考虑以下两组所有C#types的子集。 第一:
{ Animal, Tiger, Fruit, Banana }.
其次,这个明确的相关集合:
{ IEnumerable<Animal>, IEnumerable<Tiger>, IEnumerable<Fruit>, IEnumerable<Banana> }
从第一组到第二组有映射操作。 也就是说,对于第一组中的每个T,第二组中的对应types是IEnumerable<T>
。 或者简而言之,映射是T → IE<T>
。 请注意,这是一个“细箭头”。
跟我到目前为止?
现在让我们考虑一个关系 。 第一组中的成对types之间存在分配兼容性关系 。 Tiger
types的值可以分配给Animal
types的variables,因此这些types被称为“赋值兼容”。 让我们写一个简短的forms“一个X
types的值可以分配给一个Y
types的variables”: X ⇒ Y
。 请注意,这是一个“胖箭头”。
所以在我们的第一个子集中,这里是所有的赋值兼容性关系:
Tiger ⇒ Tiger Tiger ⇒ Animal Animal ⇒ Animal Banana ⇒ Banana Banana ⇒ Fruit Fruit ⇒ Fruit
在支持某些接口的协变赋值兼容性的C#4中,第二组中的types对之间有一个赋值兼容性关系:
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>
也是正确的。
如果我们在胖箭头的两边都有两个东西,那么我们可以用相应的细箭头右边的东西replace两边。
具有关于特定关系的这个属性的映射被称为“协变映射”。 这应该是有道理的:可以在需要一系列动物的地方使用一系列虎,但是相反的情况并非如此。 一连串的动物不一定要用在需要一系列虎的地方。
这是协变。 现在考虑所有types的集合的这个子集:
{ 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
),那么可以将返回的值作为更一般的types(比如object
)来处理。
逆变
典型示例: IComparer<in T>
, Action<in T>
您可以从IComparer<object>
转换为IComparer<string>
或Action<object>
为Action<string>
; 值只能进入这些对象。
这一次它的工作原理是因为如果API期望一些一般的东西(比如object
),你可以给它更特定的东西(比如string
)。
更普遍
如果你有一个接口IFoo<T>
它可以在T
是协变的(例如,如果T
只在接口中的输出位置(例如返回types)中使用,那么声明它为IFoo<out T>
,它可以在T
(即IFoo<in T>
)如果T
只用于input位置(例如参数types)。
它可能会令人困惑,因为“输出位置”不像听起来那么简单 – 一个Action<T>
types的参数仍然只在输出位置使用T
– Action<T>
的逆转会将其变成圆形,如果你明白了吗。 这是一个“输出”,值可以从方法的实现传递给调用者的代码,就像返回值一样。 通常这种事情不会出现,幸运的是:)
我希望我的文章有助于获得关于该主题的语言不可知论的观点。
对于我们的内部培训,我曾经与精彩的书“Smalltalk,Objects and Design(Chamond Liu)”一起工作,我重申了以下几个例子。
“一致性”是什么意思? 这个想法是devise具有高度可replacetypes的types安全的types层次结构。 如果您使用静态types的语言,获得此一致性的关键是基于子types的一致性。 (我们将在这里高度讨论Liskov替代原则(LSP)。)
实际的例子(在C#中的伪代码/无效):
-
协方差:让我们假设把鸡蛋与静态打字一致的鸟类:如果鸟类放置一个鸡蛋,那么鸟类亚型是不是鸡蛋的一个亚型? 例如鸭子放鸭蛋,然后一致性。 为什么这是一致的? 因为在这样一个expression式中:
Egg anEgg = aBird.Lay();
参考a鸟可以在法律上由鸟或Duck实例代替。 我们说返回types是协变的types,其中定义了Lay()。 子types的覆盖可能会返回一个更专门的types。 =>“他们提供更多。” -
逆变:让我们假设钢琴演奏者可以通过静态打字来“持续”演奏:如果钢琴演奏者弹奏钢琴,她是否能够弹奏钢琴? 难道宁愿Virtuoso玩GrandPiano? (被警告;有一个扭曲!)这是不一致的! 因为在这样的expression:
aPiano.Play(aPianist);
aPiano不能由钢琴或GrandPiano实例合法取代! 一个GrandPiano只能由Virtuoso演奏,钢琴家太一般了! GrandPianos必须可以使用更普通的types,然后才能保持一致。 我们说参数types是与Play()被定义的types不相容的。 子types的覆盖可能会接受更一般化的types。 =>“他们需要更less。”
回到C#:
因为C#基本上是一个静态types的语言,所以types接口的“位置”应该是共同的或逆变的(例如参数和返回types),必须显式标记以保证一致的使用/开发这种types,使LSP工作正常。 在dynamictypes语言中,LSP一致性通常不是问题,换句话说,如果仅在types中使用dynamictypes,则可以完全摆脱.Net接口和委托上的共同和逆变“标记”。 – 但这不是C#中最好的解决scheme(你不应该在公共接口中使用dynamic的)。
回到理论:
所描述的一致性(协变返回types/逆变参数types)是理论上的理想(由语言Emerald和POOL-1支持)。 一些语言(如埃菲尔)决定采用另一种types的一致性,尤其是 也是协变参数types,因为它比理论理想更能描述现实。 在静态types语言中,所需的一致性通常必须通过应用“双派”和“访问者”等devise模式来实现。 其他语言提供所谓的“多派遣”或多种方法(这基本上是在运行时select函数重载,例如CLOS)或使用dynamictypes来获得所需的效果。
如果要将任何方法分配给委托,方法签名必须完全匹配委托的签名。 话虽如此,协方差和逆变允许在方法的签名与代表的匹配方面有一定程度的灵活性。
你可以参考这篇文章来理解协变性,逆变性以及它们之间的差异 。
转换器代表帮助我了解其中的差异。
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
表示方法返回更具体types的 协方差 。
TInput
表示一个方法传递一个较不具体的types的 逆变 。
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();