Answers:
问题是“协方差和逆方差有什么区别?”
协方差和逆方差是将集合的一个成员与另一个成员关联的映射函数的属性。更具体地说,映射相对于关系可以是协变的或相反的该集合上。
考虑所有C#类型集的以下两个子集。第一:
{ Animal,
Tiger,
Fruit,
Banana }.
其次,这组明确相关的集合:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
从第一组到第二组有一个映射操作。也就是说,对于第一集合中的每个T,第二集合中的对应类型为IEnumerable<T>
。或者,简而言之,映射为T → IE<T>
。请注意,这是一个“细箭头”。
到目前为止和我在一起?
现在让我们考虑一个关系。第一组中的类型对之间存在分配兼容性关系。Tiger
可以将type的值分配给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>
。
如果在粗箭头的两侧都有两个东西,则可以用相应的细箭头右侧的东西替换两侧。
相对于特定关系具有此属性的映射称为“协变量映射”。这应该是有道理的:在需要一系列动物的情况下,可以使用一系列老虎,但事实并非如此。需要老虎序列时,不一定必须使用动物序列。
那是协方差。现在考虑所有类型集合的这个子集:
{ 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>
。
其中A映射蜜饯但反转的关系被称为逆变映射。
同样,这显然是正确的。可以比较两个动物的设备也可以比较两个老虎,但是可以比较两个老虎的设备不一定可以比较任何两个动物。
因此,这就是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
(即声明它IFoo<out T>
,如果T
仅在输出位置的界面内使用(例如返回类型)。它可以在逆变T
(即IFoo<in T>
)如果T
在输入位置仅用于(例如参数类型)。
这可能会造成混淆,因为“输出位置”并不像听起来那么简单-类型参数Action<T>
仍然只T
在输出位置使用- Action<T>
如果您明白我的意思,则将其四舍五入。这是在该值可以从方法的实现通过“输出” 向来电者的代码,就像一个返回值可以。通常,这种事情不会出现,幸运的是:)
Action<T>
仍然只T
在输出位置使用”。Action<T>
返回类型为void,如何将其T
用作输出?还是那是什么意思,因为它不返回任何内容,您会看到它永远不会违反规则?
我希望我的帖子有助于获得与该主题无关的语言视图。
对于我们的内部培训,我编写了很棒的书“ Smalltalk,对象和设计(Chamond Liu)”,并改写了以下示例。
“一致性”是什么意思?这个想法是设计具有高度可替换类型的类型安全类型层次结构。如果您使用静态类型的语言,则获得此一致性的关键是基于子类型的一致性。(我们将在这里高层次地讨论Liskov替代原理(LSP)。)
实际示例(伪代码/在C#中无效):
协方差:让我们假设鸟在静态类型下“一致地”产卵:如果鸟的类型产卵,那么鸟的亚型难道不会产卵吗?例如,Duck类型放置DuckEgg,则给出一致性。为什么这是一致的?因为在这样的表达式中:Egg anEgg = aBird.Lay();
引用aBird可以合法地由Bird或Duck实例替代。我们说返回类型与其中定义了Lay()的类型是协变的。子类型的替代可能会返回更特殊的类型。=>“他们提供了更多。”
矛盾:让我们假设钢琴演奏者可以使用静态类型“一致地”演奏钢琴:如果钢琴演奏者演奏钢琴,她是否可以演奏GrandPiano?维塔索不是要弹大钢琴吗?(警告;有转弯!)这是不一致的!因为这样的表达:aPiano.Play(aPianist);
aPiano不能被Piano或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()
不是有效的替代,这就是症结所在。C#不支持协变返回类型,但是Java和C ++都支持。我宁愿使用类似于C#的语法来描述理论上的理想。在C#中,您需要让Bird和Duck实现一个公共接口,在其中将Lay定义为具有协变返回(即,超出规范)类型,然后将所有问题放在一起!Egg Lay()
extends
,消费者super
”。
转换器代表帮助我理解了区别。
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<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
是子类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());