{ 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
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>
。最好通过示例来解释——这是我记住它们的方式。
协变性
经典示例: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>
)。
这可能会变得有点复杂,因为“输出位置”并不像听起来那样简单——类型为 Action<T>
的参数仍然只在输出位置使用 T
——Action<T>
的逆变性使其相反,如果你知道我的意思的话。在这种情况下,“输出”是指值可以从方法实现中向调用方的代码传递,就像返回值一样。通常情况下不需要考虑这种问题:)
Action<T>
的参数仍然只在输出位置使用T
”。 Action<T>
返回类型是void,它怎么能将T
用作输出呢?或者这就是它的意思,因为它不返回任何东西,所以你可以看到它永远不会违反规则? - Alexander Derck我希望我的文章能够给你一个与语言无关的视角。
在我们内部的培训中,我使用了一本精妙的书籍《Smalltalk, Objects and Design (Chamond Liu)》,并重新构思了以下例子。
“一致性”是什么意思?这个概念是指设计具有高度可替换类型的类型安全类型层次结构。如果您使用静态类型语言,则实现此一致性的关键是基于子类型的一致性(我们将在此高层面上讨论里氏替换原则(LSP))。
实践示例(伪代码/在 C# 中无效):
协变(Covariance):假设鸟会“一致地”下蛋,并使用静态类型:如果 Bird 类型会下蛋,那么 Bird 的子类型是否会下更专门的蛋?例如,Duck 类型会下鸭蛋,因此就具有了一致性。为何是一致的呢?因为在此类表达式中:Egg anEgg = aBird.Lay();
引用 aBird 可以被合法地替换为 Bird 或 Duck 实例。我们说返回类型对于定义 Lay() 方法的类型是协变的。子类型的覆盖方法可以返回更专门的类型。 =>“它们提供更多。”
逆变(Contravariance):假设钢琴是演奏家可以演奏的乐器,并使用静态类型:如果一个演奏家可以演奏钢琴,那么他能演奏大钢琴吗?难道不应该是技艺精湛的演奏家演奏大钢琴吗?(请注意;这里有一个转折!)这是不一致的!因为在此类表达式中:aPiano.Play(aPianist);
aPiano 不能被合法地替换为 Piano 或 GrandPiano 实例!大钢琴只能由技艺精湛的演奏家演奏,普通的演奏家太笼统了!大钢琴必须能够被更一般的类型演奏,才能保持一致。我们说参数类型对于定义 Play() 方法的类型是逆变的。子类型的覆盖方法可以接受更一般的类型。=>“它们需要更少。”
回到 C#:
因为 C# 是一种静态类型的语言,一个类型的接口中需要协变或者逆变的“位置”(比如参数和返回值)必须明确地标注出来,以保证该类型在使用/开发时的一致性,从而使LSP能够正常工作。在动态类型语言中,LSP的一致性通常不是问题。换句话说,如果您只在类型中使用了 dynamic 类型,完全可以完全摆脱 .Net 接口和委托中的协变和逆变“标记”。但是,在 C# 中这不是最好的解决方案(您不应该在公共接口中使用 dynamic)。
Bird
定义了public abstract BirdEgg Lay();
,那么Duck : Bird
必须 实现public override BirdEgg Lay(){}
。所以你断言BirdEgg anEgg = aBird.Lay();
有任何形式的差异是完全不正确的。作为解释点的前提,整个观点现在已经消失了。你能否改而说协变存在于实现中,其中一个DuckEgg被隐式转换为BirdEgg的输出/返回类型?无论哪种方式,请澄清我的困惑。 - SuamereDuckEgg Lay()
不是Egg Lay()
的有效重写,这是关键所在。C#不支持协变返回类型,但Java和C++都支持。我使用类似于C#的语法描述了理论上的理想情况。在C#中,您需要让Bird和Duck实现一个公共接口,在其中定义Lay具有协变返回(即out-specification)类型,然后问题就解决了! - Nicoextends
,消费者super
”。 - Nico协变和逆变是非常符合逻辑的事情。语言类型系统强制我们支持现实生活中的逻辑。通过例子很容易理解。
例如,你想买一朵花,在你所在的城市有两家花店:玫瑰店和雏菊店。
如果你问别人“花店在哪里?”,然后有人告诉你玫瑰店在哪里,这样可以吗?可以,因为玫瑰是花,如果你想买一朵花,你可以买一朵玫瑰花。同样的道理也适用于有人回答你让你去雏菊店的地址。
这就是协变的例子:如果A
产生泛型值(从函数返回),则允许将A<C>
强制转换为A<B>
,其中C
是B
的子类。协变是关于生产者的,这就是为什么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>
,其中 C
是 B
的子类。逆变性关注消费者,因此 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();
IBookkeeper<in T>
,我们允许分配给更具体的类型,而不是更少的类型。 in
表示数据流入对象。ICounter<out T>
,允许隐式转换为不太具体的类型,而不是更具体的类型。 out
表示数据从对象流出。
IEnumerable<Tiger>
转换为IEnumerable<Animal>
?因为没有办法向IEnumerable<Animal>
中输入一只长颈鹿。为什么我们可以将IComparable<Animal>
转换为IComparable<Tiger>
?因为没有办法从IComparable<Animal>
中取出一只长颈鹿。明白了吗? - Eric Lippert