scala中的泛型不变协变逆变

Generics invariant covariant contravariant in scala

这可能是一个很愚蠢的问题,但我挠了半天脑袋也无法理解其中的区别。

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

这里说的是

Note: subtyping of generic types is invariant. This means that if we have a stack of characters of type Stack[Char] then it cannot be used as an integer stack of type Stack[Int]. This would be unsound because it would enable us to enter true integers into the character stack. To conclude, Stack[A] is only a subtype of Stack[B] if and only if B = A.

我完全理解我不能在需要 Int 的地方使用 Char。 但是,我的 Stack class 只接受 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)

但是,在下一页 (https://docs.scala-lang.org/tour/variances.html) 上,它说类型参数应该是协变的 +A,那么 Fruit 示例是如何工作的,因为它甚至添加了带有 invariant.

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

方差更多地与复杂类型相关,而不是传递 objects,这被称为子类型。

此处解释:

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

如果你想制作一个接受某种类型的复杂类型作为接受某种其他类型的列表的 child/parent,那么方差的想法就会发挥作用。在您的示例中,它是关于传递 child 代替 parent。所以它有效。

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

请在此处查看代码。这是可以理解的。不明白请回复

这与方差完全无关。

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

这叫做子类型化,与方差完全没有关系。

让我们退一步:方差到底意味着什么?

嗯,方差 表示 "change"(想想像 "to vary" 或 "variable" 这样的词)。 co-[​​=212=] 表示 "together"(想到合作、共同教育、同地办公),contra- 表示 "against" (想到矛盾、反情报、反叛乱、避孕),in- 表示 "unrelated" 或 "non-"(想到非自愿、不可接近、不宽容) .

因此,我们有 "change" 并且该更改可以是 "together"、"against" 或 "unrelated"。嗯,为了有相关的变化,我们需要两个变化的东西,它们可以一起变化(即当一个东西变化时,另一个东西也变化"in the same direction") , 它们可以相互改变(即当一件事改变时,另一件事改变 "in the opposite direction"),或者它们可以不相关(即当一件事改变时,另一件事不变。)

这就是协变、逆变和不变的数学概念。我们只需要两个 "things",一些 "change" 的概念,而这个变化需要一些 "direction".

的概念

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

嗯,我们的两件事是什么?当我们有一个type constructor比如C[A],那么我们的两个东西就是:

  1. 类型参数A
  2. 构造类型 是将类型构造函数 C 应用于 A.
  3. 的结果

而我们在方向感上的变化是什么?是subtyping!

那么,现在的问题就变成了:"When I change A to B (along one of the directions of subtyping, i.e. make it either a subtype or a supertype), then how does C[A] relate to C[B]".

同样,存在三种可能性:

  • CovarianceA <: BC[A] <: C[B]:当 AB 的子类型时,C[A] 是 [= 的子类型26=],换句话说,当我沿子类型层次结构更改 A 时,C[A] 更改为 A 同方向.
  • ContravarianceA <: BC[A] :> C[B]:当 AB 的子类型时,那么 C[A]supertype of C[B],换句话说,当我沿子类型层次结构更改 A 时,C[A] 更改 against A 相反方向.
  • 不变性C[A]C[B]之间没有子类型关系,也不是另一个的子类型或超类型。

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

  1. 为什么这有用?
  2. 哪一个是正确的?

这与子类型有用的原因相同。实际上,这只是子类型化。所以,如果你有一种同时具有子类型和参数多态性的语言,那么了解一种类型是否是另一种类型的子类型就很重要,而方差会告诉你一个构造类型是否是另一个构造类型的子类型基于类型参数之间的子类型关系的相同构造函数。

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

让我们来看一个简单的泛型类型,一个函数。一个函数有两种类型参数,一种用于输入,一种用于输出。 (我们在这里保持简单。)F[A, B] 是一个函数,它接受类型 A 的参数和 returns 类型 B.

的结果。

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

中不协变

如果它是逆变的呢?如果我们将 F[Food, Mammal] 传递给 O 会怎样?那么,O 再次尝试传递一个 Orange 并且它有效:Orange 是一个 Food,所以 F[Food, Mammal] 知道如何处理用 Orange 秒。我们现在可以得出结论,函数在其输入中是逆变,即您可以传递一个采用更通用类型作为其输入的函数来替代采用更受限类型的函数,并且一切都会好的。

现在让我们看看F的输出。如果 FB 中像在 A 中一样是逆变的,会发生什么情况?我们将 F[Fruit, Animal] 传递给 O。根据 LSP,如果我们是对的并且函数在它们的输出中是逆变的,那么就不会发生什么不好的事情。不幸的是,OF 的结果上调用了 getMilk 方法,但 F 只是返回了一个 Chicken。哎呀。因此,函数的输出不能是逆变的。

OTOH,如果我们通过 F[Fruit, Cow] 会怎样?一切仍然有效! O对返回的奶牛调用getMilk,它确实产奶了。所以,看起来函数在它们的输出中是协变的。

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

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

事实上,这就是为什么 C♯ 的设计者选择将已经存在的关键字 inout 重新用于 variance annotations and Kotlin uses those same keywords

因此,例如,不可变集合的元素类型通常可以是协变的,因为它们不允许您将某些内容放入集合中(您只能构造一个 new 具有可能不同类型的集合)但只是为了取出元素。所以,如果我想得到一个数字列表,而有人递给我一个整数列表,我没问题。

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

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

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

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

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

假设您有一个盒子:class Box[T]

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

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

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

... it says that type parameter should be covariant +A

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