scala 中的泛型不变协变逆变 [英] Generics invariant covariant contravariant in scala

查看:58
本文介绍了scala 中的泛型不变协变逆变的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这可能是一个非常愚蠢的问题,但即使挠了很长时间,我也无法理解其中的区别.

This could be a very silly question, but I am not able to understand the difference even after scratching my head for a long time.

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

I am going through the page of scala generics: https://docs.scala-lang.org/tour/generic-classes.html

这里,据说

注意:泛型类型的子类型不变.这意味着如果我们有一堆 Stack[Char] 类型的字符,那么它不能被使用作为类型为 Stack[Int] 的整数堆栈.这将是不合理的,因为它将使我们能够将真正的整数输入到字符堆栈中.至结论是,Stack[A] 只是 Stack[B] 的一个子类型,当且仅当 B = A.

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 类只接受 A 类型(它是 invariant).如果我把苹果、香蕉或水果放进去,它们都会被接受.

I understand this completely that I cannot use Char where Int is required. But, my Stack class accepts only A type (which is invariant). If I put Apple, Banana or Fruit in them, they all are accepted.

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<的子类型/代码>.

But, on the next page (https://docs.scala-lang.org/tour/variances.html), it says that type parameter should be covariant +A, then how is the Fruit example working as even it is adding the subtypes with invariant.

希望我的问题很清楚.如果有更多信息,请告诉我.需要添加.

Hope I am clear with my question. Let me know if more Info. needs to be added.

推荐答案

这与差异完全没有关系.

This has nothing to do with variance at all.

你声明stack2是一个Stack[Fruit],换句话说,你声明你可以把任何东西放入Stack 这是一个 Fruit.AppleFruit 的(子类型),因此您可以将 Apple 放入 StackStack水果.

You declare stack2 to be a Stack[Fruit], in other words, you declare that you are allowed to put anything into the Stack which is a Fruit. An Apple is a (subtype of) Fruit, ergo you are allowed to put an Apple into a Stack of Fruits.

这被称为子类型,与变异完全没有关系.

This is called subtyping and has nothing to do with variance at all.

让我们退后一步:方差实际上意味着什么?

Let's take a step back: what does variance actually mean?

嗯,variance 的意思是改变"(想想改变"或可变"之类的词).co- 的意思是一起"(想想合作、共同教育、共处一地),contra- 意思是反对"(想想矛盾、反情报、反-insurgency、避孕),in- 的意思是无关"或非-"(想想非自愿的、无法接近的、无法容忍的).

Well, variance means "change" (think of words like "to vary" or "variable"). co- means "together" (think of cooperation, co-education, co-location), contra- means "against" (think of contradiction, counter-intelligence, counter-insurgency, contraceptive), and in- means "unrelated" or "non-" (think of involuntary, inaccessible, intolerant).

所以,我们有改变",而这种改变可以是一起"、反对"或不相关".好吧,为了有相关的变化,我们需要两个变化的东西,它们可以一起变化(即当一件事发生变化时,另一件事也朝同一方向"发生变化),它们可以互相改变(即,当一件事发生变化时,另一件事向相反的方向"发生变化),也可以无关(即当一件事发生变化时,另一件事没有发生.)

So, we have "change" and that change can be "together", "against" or "unrelated". Well, in order to have related changes, we need two things which change, and they can either change together (i.e. when one thing changes, the other thing also changes "in the same direction"), they can change against each other (i.e. when one thing changes, the other thing changes "in the opposite direction"), or they can be unrelated (i.e. when one thing changes, the other doesn't.)

这就是协方差、逆变和不变性的数学概念的全部内容.我们只需要两个东西",一个变化"的概念,而这个变化需要一些方向"的概念.

And that's all there is to the mathematical concept of covariance, contravariance, and invariance. All we need are two "things", some notion of "change", and this change needs to have some notion of "direction".

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

Now, that's of course very abstract. In this particular instance, we are talking about the context of subtyping and parametric polymorphism. How does this apply here?

那么,我们的两件事是什么?当我们有一个类型构造函数,比如C[A],那么我们的两件事是:

Well, what are our two things? When we have a type constructor such as C[A], then our two things are:

  1. 类型参数 A.
  2. 构造类型,它是将类型构造函数 C 应用于 A 的结果.
  1. The type argument A.
  2. The constructed type which is the result of applying the type constructor C to A.

我们对方向感的改变是什么?这是子类型

And what is our change with a sense of direction? It is subtyping!

所以,现在的问题变成了:当我将 A 更改为 B 时(沿着子类型的方向之一,即使其成为子类型或超类型),那么 C[A]C[B] 有什么关系".

So, the question now becomes: "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]".

再说一次,有三种可能:

And again, there are three possibilities:

  • 协方差:A <:BC[A] <: C[B]:当 AB 的子类型时,C[A]C[B] 的子类型,换句话说,当我沿着子类型层次改变 A 时,C[A] A相同的方向变化.
  • 逆变:A <:BC[A] :>C[B]:当AB的子类型时,C[A]超类型<C[B] 的/em>,换句话说,当我沿着子类型层次改变 A 时,C[A] 就会改变 反对 A相反的方向.
  • 不变性:C[A]C[B]之间没有子类型关系,既不是子类型也不是超类型另一个.
  • Covariance: A <: BC[A] <: C[B]: when A is a subtype of B then C[A] is a subtype of C[B], in other words, when I change A along the subtyping hierarchy, then C[A] changes with A in the same direction.
  • Contravariance: A <: BC[A] :> C[B]: when A is a subtype of B, then C[A] is a supertype of C[B], in other words, when I change A along the subtyping hierarchy, then C[A] changes against A in the opposite direction.
  • Invariance: there is no subtyping relationship between C[A] and C[B], neither is a sub- nor supertype of the other.

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

There are two questions you might ask yourself now:

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

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

This is useful for the same reason subtyping is useful. In fact, this is just subtyping. So, if you have a language which has both subtyping and parametric polymorphism, then it is important to know whether one type is a subtype of another type, and variance tells you whether or not a constructed type is a subtype of another constructed type of the same constructor based on the subtyping relationship between the type arguments.

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

Which one is the right one is trickier, but thankfully, we have a powerful tool for analyzing when a subtype is a subtype of another type: Barbara Liskov's Substitution Principle tells us that a type S is a subtype of type T IFF any instance of T can be replaced with an instance of S without changing the observable desirable properties of the program.

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

Let's take a simple generic type, a function. A function has two type parameters, one for the input, and one for the output. (We are keeping it simple here.) F[A, B] is a function that takes in an argument of type A and returns a result of type B.

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

And now we play through a couple of scenarios. I have some operation O that wants to work with a function from Fruits to Mammals (yeah, I know, exciting original examples!) The LSP says that I should also be able to pass in a subtype of that function, and everything should still work. Let's say, F were covariant in A. Then I should be able to pass in a function from Apples to Mammals as well. But what happens when O passes an Orange to F? That should be allowed! O was able to pass an Orange to F[Fruit, Mammal] because Orange is a subtype of Fruit. But, a function from Apples doesn't know how to deal with Oranges, so it blows up. The LSP says it should work though, which means that the only conclusion we can draw is that our assumption is wrong: F[Apple, Mammal] is not a subtype of F[Fruit, Mammal], in other words, F is not covariant in A.

如果它是逆变的呢?如果我们将 F[Food, Mammal] 传递给 O 会怎样?好吧,O 再次尝试传递一个 Orange 并且它起作用了:Orange 是一个 Food,所以 F[Food, Mammal] 知道如何处理Orange.我们现在可以得出结论,函数在其输入中逆变,也就是说,您可以传递一个将更通用类型作为其输入的函数来替代接受更受限制类型的函数,一切都会正常工作很好.

What if it were contravariant? What if we pass an F[Food, Mammal] into O? Well, O again tries to pass an Orange and it works: Orange is a Food, so F[Food, Mammal] knows how to deal with Oranges. We can now conclude that functions are contravariant in their inputs, i.e. you can pass a function that takes a more general type as its input as a replacement for a function that takes a more restricted type and everything will work out fine.

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

Now let's look at the output of F. What would happen if F were contravariant in B just like it is in A? We pass an F[Fruit, Animal] to O. According to the LSP, if we are right and functions are contravariant in their output, nothing bad should happen. Unfortunately, O calls the getMilk method on the result of F, but F just returned it a Chicken. Oops. Ergo, functions can't be contravariant in their outputs.

OTOH,如果我们传递一个 F[Fruit, Cow] 会发生什么?一切仍然有效!O 对返回的奶牛调用 getMilk,它确实提供了牛奶.因此,看起来函数的输出是协变的.

OTOH, what happens if we pass an F[Fruit, Cow]? Everything still works! O calls getMilk on the returned cow, and it indeed gives milk. So, it looks like functions are covariant in their outputs.

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

And that is a general rule that applies to variance:

  • A IFF中使C[A] 协变是安全的(在LSP的意义上)strong> A 用作输出.
  • A IFF中使C[A] 逆变是安全的(在LSP的意义上)strong> A 用作输入.
  • 如果A既可以用作输入也可以用作输出,那么C[A] 必须中是不变的A,否则结果不安全.
  • It is safe (in the sense of the LSP) to make C[A] covariant in A IFF A is used only as an output.
  • It is safe (in the sense of the LSP) to make C[A] contravariant in A IFF A is used only as an input.
  • If A can be used either as an input or as an output, then C[A] must be invariant in A, otherwise the result is not safe.

事实上,这就是为什么 C♯ 的设计者选择将已经存在的关键字 inout 重新用于 方差注释Kotlin 使用相同的关键字.

In fact, that's why C♯'s designers chose to re-use the already existing keywords in and out for variance annotations and Kotlin uses those same keywords.

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

So, for example, immutable collections can generally be covariant in their element type, since they don't allow you to put something into the collection (you can only construct a new collection with a potentially different type) but only to get elements out. So, if I want to get a list of numbers, and someone hands me a list of integers, I am fine.

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

On the other hand, think of an output stream (such as a Logger), where you can only put stuff in but not get it out. For this, it is safe to be contravariant. I.e. if I expect to be able to print strings, and someone hands me a printer that can print any object, then it can also print strings, and I am fine. Other examples are comparison functions (you only put generics in, the output is fixed to be a boolean or an enum or an integer or whatever design your particular language chooses). Or predicates, they only have generic inputs, the output is always fixed to be a boolean.

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

But, for example, mutable collections, where you can both put stuff in and get stuff out, are only type-safe when they are invariant. There are a great many tutorials explaining in detail how to break Java's or C♯'s type-safety using their covariant mutable arrays, for example.

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

Note, however that it is not always obvious whether a type is an input or an output once you get to more complex types. For example, when your type parameter is used as the upper or lower bound of an abstract type member, or when you have a method which takes a function that returns a function whose argument type is your type parameter.

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

Now, to come back to your question: you only have one stack. You never ask whether one stack is a subtype of another stack. Therefore, variance doesn't come into play in your example.

这篇关于scala 中的泛型不变协变逆变的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆