协方差和反方差之间的差异


Answers:


266

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

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

考虑所有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中协方差和相反方差的区别。协方差保留了可分配性的方向。逆差反转


4
对于像我这样的人,最好添加一些示例,以显示什么不是协变的,什么不是协变的以及什么不是两者。
bjan 2015年

2
@Bargitta:这非常相似。区别在于C#使用确定的站点差异,而Java使用调用站点的差异。因此,变化的方式是相同的,但是开发人员说“我需要将此作为变体”的地方不同。顺便说一句,两种语言的功能部分是由同一个人设计的!
埃里克·利珀特

2
@AshishNegi:将箭头读为“可以用作”。“可以比较动物的东西可以用作可以比较老虎的东西”。现在有意义吗?
埃里克·利珀特

1
@AshishNegi:不,那是不对的。 IEnumerable是协变的,因为T仅出现在IEnumerable方法的返回值中。 IComparable是互变的,因为T仅作为IComparable方法的形式参数出现
埃里克·利珀特

2
@AshishNegi:您想考虑这些关系背后的逻辑原因为什么我们IEnumerable<Tiger>可以IEnumerable<Animal>安全地转换?因为没有办法将长颈鹿输入其中IEnumerable<Animal>。为什么我们可以将转换IComparable<Animal>IComparable<Tiger>?因为没有办法一只狮子中取出长颈鹿IComparable<Animal>。合理?
埃里克·利珀特

111

举个例子可能是最容易的-我当然记得这些例子。

协方差

典型示例: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>如果您明白我的意思,则将其四舍五入。这是在该值可以从方法的实现通过“输出” 来电者的代码,就像一个返回值可以。通常,这种事情不会出现,幸运的是:)


1
对于像我这样的人,最好添加一些示例,以显示什么不是协变的,什么不是协变的以及什么不是两者。
bjan

1
@Jon Skeet尼斯的例子,我只听不懂“类型的参数Action<T>仍然只T在输出位置使用”Action<T>返回类型为void,如何将其T用作输出?还是那是什么意思,因为它不返回任何内容,您会看到它永远不会违反规则?
Alexander Derck '16

2
对于我自己的未来,我再次回到这个极好的答案以重新学习差异,这是您想要的行:“ [协方差]有效,因为如果您仅从API中提取值,它将返回一些值特定值(例如字符串),您可以将该返回值视为更通用的类型(例如对象)。”
马特·克莱因

所有这一切中最令人困惑的部分是,对于协方差还是对方差,如果忽略方向(从内到外),无论如何,您将获得“更具体到更通用”的转换!我的意思是:“你可以把这个返回值作为一个更普遍的类型(如对象)”的协方差和:“API期待一般的东西(如对象),你可以给它一些更具体的(如字符串)”为逆变。对我来说,这些听起来是一样的!
XMight'17

@AlexanderDerck:不确定为什么我之前没有回复您;我同意目前尚不清楚,并将尝试对其进行澄清。
乔恩·斯基特

16

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

对于我们的内部培训,我编写了很棒的书“ 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输出/返回类型的实现中存在协方差?无论哪种方式,请清除我的困惑。
Suamere 2015年

1
简而言之:您是对的!对困惑感到抱歉。在C#中DuckEgg Lay()不是有效的替代,这就是症结所在。C#不支持协变返回类型,但是Java和C ++都支持。我宁愿使用类似于C#的语法来描述理论上的理想。在C#中,您需要让Bird和Duck实现一个公共接口,在其中将Lay定义为具有协变返回(即,超出规范)类型,然后将所有问题放在一起!Egg Lay()
Nico 2015年

1
类似于马特·克莱因(Matt-Klein)对@乔恩·斯凯特(Jon-Skeet)的回答“对我未来的自我”的评论:在这里,对我而言,最好的选择是“它们提供更多”(特定)和“它们需要较少”(特定)。“少要求多交付”是一个绝妙的记忆!这类似于一项工作,在该工作中,我希望只要求较少的说明(一般要求),而要提供更具体的说明(实际的工作产品)。无论哪种方式,子类型(LSP)的顺序都不会中断。
karfus,

@karfus:谢谢,但是我记得我从另一个来源解释了“要求更少并提供更多”的想法。可能是我上面提到的Liu的书……甚至是.NET Rock的话题。顺便说一句。在Java中,人们将助记符简化为“ PECS”,这与声明方差的句法直接相关,PECS用于“生产者extends,消费者super”。
Nico

5

转换器代表帮助我理解了区别。

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

0

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

协方差

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

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

这是协方差的示例:如果产生通用值(从函数返回),则可以强制转换A<C>A<B>,其中C是的子类。协方差与生产者有关,这就是C#使用关键字进行协方差的原因。BAout

类型:

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

链接

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.