scala中的泛型不变协变逆变

时间:2018-05-12 05:15:24

标签: scala scala-collections

这可能是一个非常愚蠢的问题,但即使长时间挠头后我也无法理解其中的差异。

我正在浏览scala generics页面:https://docs.scala-lang.org/tour/generic-classes.html

这里,据说

  

注意:泛型类型的子类型是不变。这意味着,如果我们   有一堆Stack [Char]类型的字符然后它就无法使用   作为Stack [Int]类型的整数堆栈。这将是不健全的,因为   它将使我们能够在字符堆栈中输入真正的整数。至   总而言之,Stack [A]只是Stack [B]的一个子类型,当且仅当B = A。

我完全理解这一点,我无法使用Char,而Int则需要Stack。 但是,我的A类只接受invariant类型(class Fruit class Apple extends Fruit class Banana extends Fruit val stack2 = new Stack[Fruit] stack2.push(new Fruit) stack2.push(new Banana) stack2.push(new Apple) )。如果我把苹果,香蕉或水果放入其中,它们都会被接受。

+A

但是,在下一页(https://docs.scala-lang.org/tour/variances.html)上,它表示类型参数应该是协变invariant,那么即使添加带有{{1}的子类型,Fruit示例如何工作呢? }。

希望我对我的问题很清楚。如果有更多信息,请告诉我。需要补充。

3 个答案:

答案 0 :(得分:3)

这与方差无关。

您声明stack2Stack[Fruit],换句话说,您声明允许您将任何内容放入Stack FruitAppleFruit的(子类型),因此您可以将Apple放入Stack Fruits

这称为子类型,与 variance 完全无关。

让我们退后一步:差异究竟意味着什么?

嗯, variance 意味着"改变" (想想像"改变"或"变量")。 co - 意味着"在一起" (想想合作,共同教育,共同定位), contra - 意味着"反对" (想想矛盾,反情报,反叛乱,避孕), in - 意味着"无关"或者"非 - " (想想非自愿,无法接近,不宽容)。

所以,我们已经改变了#34;而且这种变化可以和#34;,#34;对抗"或"无关"。好吧,为了进行相关的更改,我们需要两个更改的内容,它们可以一起更改(即,当一个事情发生变化时,另一个事情也会发生变化"在同一个方向上&# 34;),他们可以相互改变(即当一件事发生变化时,另一件事发生变化"在相反的方向上#34;),或者他们可能是不相关的(即当一件事发生变化时,另一件事不会&# 39; t)的

这就是协方差,逆变和不变性的数学概念。我们所需要的只是两个"事物",一些概念"改变",这个改变需要有一些概念"方向"。

现在,这当然非常抽象。在 this 特定实例中,我们讨论的是子类型和参数多态的上下文。这在哪里适用?

嗯,我们的两件事是什么?如果我们有type constructor,例如C[A],那么我们的两件事就是:

  1. 类型参数A
  2. 构造类型,它是将类型构造函数C应用于A的结果。
  3. 我们的方向感有什么变化?这是subtyping

    所以,问题现在变成:"当我将A更改为B时(沿着子类型的一个方向,即使其成为子类型或超类型),那么如何C[A]C[B]"。

    有关

    同样,有三种可能性:

    • CovarianceA <: BC[A] <: C[B]:当AB的子类型时,C[A]C[B]的子类型换句话说,当我在子类型层次结构中更改A时,C[A]同一方向中使用 A更改
    • ContravarianceA <: BC[A] :> C[B]:当AB的子类型时,C[A]超类型<是C[B]的/ em>,换句话说,当我在子类型层次结构中更改A时,C[A]会在<{1}}中更改 A em>相反的方向
    • 不变性C[A]C[B]之间没有子类型关系,也不是另一种的子类型或超类型。

    您现在可能会问两个问题:

    1. 为什么这有用?
    2. 哪一个是正确的?
    3. 这与子类型有用的原因相同。实际上,这只是一种分类。因此,如果您的语言同时包含子类型和参数多态,那么了解一种类型是否是另一种类型的子类型非常重要,并且方差会告诉您构造的类型是否是另一种类型的子类型。基于类型参数之间的子类型关系的相同构造函数。

      哪一个是正确的,但是谢天谢地,我们有一个强大的工具来分析子类型何时是另一种类型的子类型:Barbara Liskov's Substitution Principle告诉我们类型S是一个子类型类型T IFF T的任何实例都可以替换为S的实例,而不会更改程序的可观察的理想属性。

      让我们采用一种简单的泛型,一种功能。函数有两个类型参数,一个用于输入,另一个用于输出。 (我们在这里保持简单。)F[A, B]是一个函数,它接受类型A的参数并返回类型B的结果。

      现在我们玩了几个场景。我有一些操作 O 想要使用从FruitMammal的函数(是的,我知道,令人兴奋的原始示例!)LSP说我应该也能够传递该函数的子类型,一切都应该仍然有效。我们可以说,FA中是协变的。然后我应该能够将Apple s中的函数传递给Mammal s。但是当 O Orange传递给F时会发生什么?那应该被允许! O 能够将Orange传递给F[Fruit, Mammal],因为OrangeFruit的子类型。但是,来自Apple的函数并不知道如何处理Orange,所以它会爆炸。 LSP说它应该工作,这意味着我们可以得出的唯一结论是我们的假设是错误的:F[Apple, Mammal]不是F[Fruit, Mammal]的子类型,换句话说,FA中没有协变。

      如果是逆变怎么办?如果我们将F[Food, Mammal]传递给 O 怎么办?好吧, O 再次尝试传递Orange并且它有效:OrangeFood,因此F[Food, Mammal]知道如何处理Orange 1}}秒。我们现在可以得出结论,函数在它们的输入中是逆变,即你可以传递一个函数,它将一个更通用的类型作为输入,作为一个更受限制类型的函数的替代,一切都会工作好的。

      现在让我们看一下F的输出。如果F BA中与F[Fruit, Animal]中的逆变一样,会发生什么?我们将getMilk传递给 O 。根据LSP,如果我们是正确的并且功能在其输出中是逆变的,那么应该发生任何不好的事情。很遗憾, O 会在F的结果上调用F方法,但Chicken只返回F[Fruit, Cow]。哎呀。因此,功能在其输出中不能逆变。

      OTOH,如果我们通过getMilk会怎样?一切仍然有效! O 在返回的牛身上调用C[A],它确实会产生牛奶。因此,看起来函数在其输出中是协变的。

      这是适用于差异的一般规则:

      • 使用A IFF A中的C[A] 协变是安全的(在LSP的意义上) 作为输出。
      • 使用A IFF A A 逆变是安全的(在LSP的意义上) 作为输入。
      • 如果C[A]可用作输入或输出,则A 必须in中保持不变,否则结果不安全

      事实上,这就是为什么C♯的设计师选择重复使用已存在的关键字outLogger作为variance annotations和{{3 }}

      因此,例如,不可变集合通常在其元素类型中是协变的,因为它们不允许您将某些内容放入集合中(您只能构建一个 new 集合一种可能不同的类型)但只是为了获取元素。所以,如果我想得到一个数字列表,并且有人给我一个整数列表,我很好。

      另一方面,考虑输出流(例如{{1}}),您只能在中放置但不能将其取出。为此,逆变是安全的。即如果我希望能够打印字符串,并且有人给我打印机可以打印任何对象,那么它也可以打印字符串,我很好。其他示例是比较函数(您只将泛型放在中,输出固定为布尔值或枚举或整数或您的特定语言选择的任何设计)。或谓词,它们只有通用输入,输出总是固定为布尔值。

      但是,例如, mutable 集合,你可以把东西放入并获取东西,只有在它们不变时才是类型安全的。例如,有很多教程详细解释了如何使用协变可变数组来破解Java或C♯的类型安全性。

      但是,请注意,一旦获得更复杂的类型,类型是输入还是输出并不总是很明显。例如,当您的type参数用作抽象类型成员的上限或下限时,或者当您有一个方法接受一个返回其参数类型为您的类型参数的函数的函数时。

      现在,回到你的问题:你只有一个堆栈。你永远不会问一个堆栈是否是另一个堆栈的子类型。因此,在你的例子中,方差并没有发挥作用。

答案 1 :(得分:3)

关于Scala类型方差的一个非显而易见的事情是,注释+A-A实际上告诉我们有关包装器的更多信息,而不是类型参数。

我们假设您有一个方框:class Box[T]

因为T是不变的,这意味着某些Box[Apple]Box[Fruit]无关。

现在让它变得协变:class Box[+T]

这有两件事,它限制了Box代码在内部使用T的方式,但更重要的是,它改变了各种Box实例之间的关系。特别是,Box[Apple]类型现在是Box[Fruit]的子类型,因为AppleFruit的子类型,我们已经指示Box 1}}以相同的方式改变其类型关系(即&#34; co - &#34;)作为其类型参数。

  

...它说类型参数应该是协变的+A

实际上,Stack代码不能成为共同或反对变体。正如我所提到的,方差注释对使用类型参数的方式添加了一些限制,并且Stack代码使用A的方式与共方差和反方差相反。

答案 2 :(得分:-1)

方差更多地与复杂类型相关,而不是传递称为子类型的对象。

在这里解释:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29

如果你想创建一个复杂的类型,它接受某种类型作为接受某种其他类型的列表的子/父,那么方差的概念就会产生影响。在你的例子中,它是关于传递孩子代替父母。所以它有效。

https://coderwall.com/p/dlqvnq/simple-example-for-scala-covariance-contravariance-and-invariance

请在此处查看代码。这是可以理解的。如果你没有得到它,请回复。