协方差与;逆变

时间:2010-02-02 14:07:34

标签: c# c#-4.0 covariance contravariance

我无法理解协方差和逆变之间的区别。

7 个答案:

答案 0 :(得分:254)

问题是“协方差和逆变之间有什么区别?”

协方差和逆变是映射函数的属性,它将一个集合中的一个成员与另一个成员相关联。更具体地说,映射对于该集合上的关系可以是协变的或逆变的。

考虑所有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>也是如此。

如果我们在胖箭的两侧有两个东西,那么我们可以用相应的细箭头右侧的东西替换两侧。

具有关于特定关系的该属性的映射称为“协变映射”。这应该是有道理的:可以在需要动物序列的地方使用一系列老虎,但事实恰恰相反。在需要一系列老虎的情况下,不一定能使用一系列动物。

这是协方差。现在考虑所有类型集的这个子集:

{ 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中协方差和逆变的区别。协方差保留了可转让性的方向。逆变逆转

答案 1 :(得分:105)

举出例子可能最容易 - 这当然是我记得他们的方式。

<强>协方差

典型示例:IEnumerable<out T>Func<out T>

您可以从IEnumerable<string>转换为IEnumerable<object>,或Func<string>转换为Func<object>。值只来自 out 这些对象。

它的工作原理是因为如果你只是从API中取值,并且它会返回特定的东西(比如string),你可以将返回的值视为更通用的类型(如{{1 }})。

<强>逆变

典型示例:objectIComparer<in T>

您可以从Action<in T>转换为IComparer<object>,或IComparer<string>转换为Action<object>;值只会进入这些对象。

这一次是有效的,因为如果API期望一般性(例如Action<string>),你可以给它一些更具体的东西(比如object)。

更一般地

如果你有一个接口string,它可以在IFoo<T>中协变(即如果T仅用于输出位置(例如返回),则将其声明为IFoo<out T>如果T仅用于输入位置(例如参数类型),它可以在T(即IFoo<in T>)中逆变。

它可能会让人感到困惑,因为“输出位置”并不像听起来那么简单 - T类型的参数仍然只在输出位置使用Action<T> - {{}的逆变1}}如果你明白我的意思,就把它转过来。这是一个“输出”,因为值可以从方法的实现转移到调用者的代码,就像返回值一样。通常这种事情不会出现,幸运的是:)

答案 2 :(得分:14)

我希望我的帖子有助于获得与该主题无关的语言。

对于我们的内部培训,我使用了精彩的书籍“Smalltalk,Objects and Design(Chamond Liu)”,并重述了以下示例。

“一致性”是什么意思?我们的想法是设计具有高度可替换类型的类型安全类型层次结构。如果您使用静态类型语言,获得此一致性的关键是基于子类型的一致性。 (我们将在这里高层讨论Liskov替换原则(LSP)。)

实际例子(伪代码/ C#中无效):

  • 协方差:让我们假设那些用“静态”打字“持续”生产鸡蛋的鸟类:如果Bird放置一个蛋类型,那么Bird的子类型不会产生蛋类型吗?例如。 Duck类型放置DuckEgg,然后给出一致性。为什么这一致?因为在这样的表达式中:Egg anEgg = aBird.Lay();引用aBird可以合法地由Bird或Duck实例替换。我们说返回类型与类型协变,其中定义了Lay()。子类型的覆盖可以返回更专用的类型。 =&GT; “他们提供更多。”

  • 逆变法:让我们假设钢琴家可以“静态地”玩“钢琴演奏家”:如果钢琴家演奏钢琴,她能演奏GrandPiano吗?是不是宁愿Virtuoso演奏GrandPiano? (警告;有一个扭曲!)这是不一致的!因为在这样的表达中:aPiano.Play(aPianist); aPiano不能被钢琴或GrandPiano实例合法替代! GrandPiano只能由Virtuoso演奏,钢琴家太一般了! GrandPianos必须可以通过更一般的类型播放,然后播放是一致的。我们说参数类型与类型是逆变的,其中定义了Play()。子类型的覆盖可以接受更通用的类型。 =&GT; “他们需要更少。”

回到C#:
因为C#基本上是静态类型语言,所以类型接口的“位置”应该是共变或逆变的(例如参数和返回类型),必须明确标记以保证该类型的一致使用/开发,以使LSP工作正常。在动态类型语言中,LSP一致性通常不是问题,换句话说,如果您只在类型中使用动态类型,则可以完全摆脱.Net接口和委托上的共变和逆变“标记”。 - 但这不是C#中最好的解决方案(你不应该在公共接口中使用动态)。

回到理论:
所描述的一致性(协变返回类型/逆变参数类型)是理论上的理想(由Emerald和POOL-1语言支持)。一些oop语言(例如Eiffel)决定应用另一种类型的一致性,尤其是。还有协变参数类型,因为它比理论理想更能描述现实。 在静态类型语言中,通常必须通过应用诸如“双调度”和“访问者”之类的设计模式来实现期望的一致性。其他语言提供所谓的“多次调度”或多种方法(这基本上是在运行时选择函数重载,例如使用CLOS)或通过使用动态类型获得所需的效果。

答案 3 :(得分:4)

如果要将任何方法分配给委托,则方法签名必须与委托的签名完全匹配。话虽如此,协方差和逆变在允许方法签名与代表签名相匹配方面具有一定程度的灵活性。

您可以参考此article to understand covariance, contravariance, and the differences between them

答案 4 :(得分:3)

转换器代表帮助我理解其中的差异。

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();

答案 5 :(得分:0)

Co和Contra方差是很合逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过示例很容易理解。

协方差

例如,您想购买一朵花,而您所在的城市有两家花店:玫瑰店和雏菊店。

如果您问某人“花店在哪里?”有人告诉你玫瑰花店在哪里,可以吗?是的,因为玫瑰是一朵花,所以如果您想买花,可以买玫瑰。如果有人用菊花店的地址答复您,则同样适用。

这是协方差的示例:您可以将A<C>强制转换为A<B>,其中CB的子类,如果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();
}

Contravariance

例如,您想给女友送一朵花,而女友喜欢任何花。您可以将她视为爱玫瑰的人,还是爱雏菊的人?是的,因为如果她喜欢任何花,她都会喜欢玫瑰和雏菊。

这是 contravariance 的示例:您可以将A<B>强制转换为A<C>,其中CB的子类,如果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());

链接

答案 6 :(得分:-1)

考虑一个组织中有两个职位。爱丽丝是椅子的柜台。而鲍勃是同一把椅子的店主。

逆变。现在我们不能将鲍勃命名为家具店主,因为他不会把桌子带到他的商店,他只会存放椅子。但是我们可以称他为紫色椅子的店主,因为紫色椅子是椅子。这是 IBookkeeper<in T>,我们允许分配给更具体的类型而不是更少。 in 代表数据流入对象。

协方差。相反,我们可以将 Alice 命名为家具柜台,因为它不会影响她的角色。但是我们不能将她命名为红色椅子的计数器,因为我们会期望她不会数非红色椅子,但她会数它们。这是 ICounter<out T>,允许隐式转换为不太具体,而不是更具体。 out 代表数据流出对象。

不变性是指我们不能两者兼得。