当前位置: 首页 > 知识库问答 >
问题:

scala中的泛型不变、协变、逆变

司马奇希
2023-03-14

这可能是一个很傻的问题,但我挠头了很久也弄不明白其中的区别。

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

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

我完全理解这一点,我不能在需要int的地方使用char。但是,我的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)

但是,在下一页(https://docs.scala-lang.org/tour/variances.html),它说类型参数应该是covariant+a,那么Fruit示例在添加带有invariant的子类型时是如何工作的。

希望我的问题讲清楚了。如果有更多信息请告诉我。需要添加。

共有1个答案

孙夕
2023-03-14

这与方差一点关系都没有。

您声明stack2堆栈[Fruit],换句话说,您声明允许将任何内容放入堆栈中,该堆栈是Fruit苹果水果的(子类型),因此允许将苹果放入水果堆栈中。

这被称为子类型,与方差完全无关。

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

嗯,variance的意思是“改变”(想想像“to variance”或“variable”这样的词)。Co--意味着“在一起”(想想合作、共同教育、同地办公),contra--意味着“反对”(想想矛盾、反情报、反叛乱、避孕),in--意味着“不相关”或“非相关”(想想非自愿、不可接近、不容忍)。

所以,我们有了“变化”,而这种变化可以是“共同的”、“反对的”或“不相关的”。好吧,为了有相关的变化,我们需要两个变化的事物,它们可以一起变化(即当一个事物变化时,另一个事物也在“相同的方向”变化),它们可以相互变化(即当一个事物变化时,另一个事物在“相反的方向”变化),或者它们可以不相关(即当一个事物变化时,另一个事物不变)

这就是关于协方差、逆方差和不变性的数学概念。我们所需要的只是两个“东西”,一些“改变”的概念,而这种改变需要有一些“方向”的概念。

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

好吧,我们的两件事是什么?当我们有一个类型构造函数,如c[a]时,我们的两件事是:

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

而我们有方向感的变化是什么?它是子类型!

再说一遍,有三种可能性:

  • 协方差:A<:BC[A]<:c[B]:当AB的子类型时,则C[A]C[B]的子类型,换句话说,当我沿着子类型层次结构更改A时,则C[A]A在同一方向上更改。
  • 反向性:A<:BC[A]:>C[B]:当AB的子类型时,则C[A]C[B]的超类型,换句话说,当我沿着子类型层次结构更改A时,则C[A]A反向更改。
  • 不变性:C[A]C[B]之间不存在子类型关系,既不是对方的子类型,也不是对方的超类型。

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

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

这是有用的,因为同样的原因子类型是有用的。实际上,这只是子类型。因此,如果您的语言同时具有子类型和参数多态性,那么了解一个类型是否是另一个类型的子类型是很重要的,而方差根据类型参数之间的子类型关系告诉您一个构造类型是否是同一个构造函数的另一个构造类型的子类型。

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

现在我们玩了几个场景。我有一些操作O想要处理从fruits到mammals的函数(是的,我知道,激动人心的原始示例!)LSP说,我也应该能够传入该函数的子类型,并且一切都应该仍然可以工作。假设fa中是协变的。那么我也可以将apple的函数传递给mammal。但是当O将橙色传递给F时会发生什么呢?那是应该允许的!O能够将orange传递给f[Fruit,Mammal],因为orangeFruit的一个子类型。但是,一个来自apples的函数不知道如何处理oranges,所以它爆炸了。LSP说它应该起作用,这意味着我们能得出的唯一结论是我们的假设是错误的:F[苹果,哺乳动物]不是F[水果,哺乳动物]的子类型,换句话说,Fa中不是协变的。

如果它是逆变的呢?如果我们将f[Food,Mammal]传递给O会怎样?那么,O再次尝试传递一个橙色并且它起作用了:橙色是一个食物所以f[Food,Mammal]知道如何处理橙色s。现在我们可以得出结论,函数的输入是反向的,也就是说,您可以传递一个以更一般类型作为输入的函数,以替换一个以更受限制的类型作为输入的函数,那么所有的事情都会处理得很好。

现在让我们看看f的输出。如果fb中像在a中一样是反向的,会发生什么?我们将f[Fruit,Animal]传递给O。根据LSP,如果我们是正确的,并且函数的输出是反向的,那么就不会发生什么坏事。不幸的是,O对f的结果调用getmilk方法,但f只是给它返回了一个chicken。哎呀。因此,函数的输出不可能是逆变的。

哦,如果我们通过一个f[水果,奶牛]会发生什么?一切还正常!O在返回的奶牛上调用getmilk,它确实给出了牛奶。所以,看起来函数的输出是协变的。

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

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

事实上,这就是为什么C的设计人员选择重用已经存在的关键字Inout用于方差注释,Kotlin使用了这些相同的关键字。

另一方面,考虑一个输出流(例如logger),在这里只能将内容放入而不能取出。对此,逆变是安全的。即。如果我期望能够打印字符串,有人递给我一台可以打印任何html" target="_blank">对象的打印机,那么它也可以打印字符串,我就没事了。其他例子是比较函数(您只放入泛型,输出固定为布尔值、枚举或整数或您的特定语言选择的任何设计)。或谓词,它们只有泛型输入,输出总是固定为布尔值。

但是,例如,可变集合,你可以把东西放进去,也可以把东西放出来,只有当它们是不变的时候,它们才是类型安全的。例如,有很多教程详细解释了如何使用Java或C的协变可变数组来破坏它们的类型安全。

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

现在,回到您的问题上来:您只有一个堆栈。您从不询问一个堆栈是否是另一个堆栈的子类型。因此,方差在您的示例中不起作用。

 类似资料:
  • 我是Scala的新手。我在想整个逆变关系是如何运作的。我了解协方差和不变量的概念,我也知道如何在实践中实现它们。我还理解了逆变(协方差的反向)的概念,以及它是如何在Scala中的Function1特性中实现的。它为您提供了一种抽象,而无需为不同的类重新定义Function1实现。但是,我还是不完全明白,奇怪吗?现在,我就快到了…我如何用逆变来解决下面的问题: 上面的例子摘自http://blog.

  • 此代码只是使用中间的来删除重复项,其中元素之间的相等性是根据提供的比较器定义的。 让我们给局部类型推断一个机会吧,我(天真地)想...于是我将上面的代码改为: 这对我来说是有意义的,因为的类型可以从的类型推断出来,或者我是这么想的。但是,修改后的代码无法编译,并生成以下错误: 注意1:编译代码的一种方法是将返回类型更改为。不过,那是一套很难用的... 注意2:另一种方法是在比较器中不使用逆变,但我

  • 本文向大家介绍C# 泛型接口的抗变和协变,包括了C# 泛型接口的抗变和协变的使用技巧和注意事项,需要的朋友参考一下 1, 泛型接口的协变 如果泛型类型用out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。 泛型接口的抗变 如果泛型类型用in关键字标注,泛型接口就是抗变的。这样,接口只能把泛型类型T用作其方法的输入,即方法的参数。 这是泛型接口的抗变和协变的定义,那我们下面来用代码说明

  • 本文向大家介绍详解c# 协变和逆变,包括了详解c# 协变和逆变的使用技巧和注意事项,需要的朋友参考一下 基本概念 协变:能够使用比原始指定的派生类型的派生程度更大(更具体)的类型。例如 IFoo<父类> = IFoo<子类> 逆变:能够使用比原始指定的派生类型的派生程度更新(更抽象)的类型。例如 IBar<子类> = IBar<父类> 关键字out和in 协变和逆变在泛型参数中的表现方式,out关

  • 本文向大家介绍C#中的协变与逆变深入讲解,包括了C#中的协变与逆变深入讲解的使用技巧和注意事项,需要的朋友参考一下 什么是协变与逆变 MSDN的解释: https://msdn.microsoft.com/zh-cn/library/dd799517.aspx 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大

  • 从Joshua Bloch的Effective Java中, > 数组与泛型类型有两个重要的区别。第一个数组是协变的。泛型是不变的。 协变简单地说,如果X是Y的子型,那么X[]也将是Y[]的子型。数组是协变的,因为字符串是对象的子类型,所以 不变简单地说,不管X是不是Y的子类型, 我的问题是为什么决定在Java中使数组是协变的?还有其他的SO帖子,比如为什么数组是不变的,但是列表是协变的?,但它们