为啥数组是不变的,而列表是协变的?

Posted

技术标签:

【中文标题】为啥数组是不变的,而列表是协变的?【英文标题】:Why are Arrays invariant, but Lists covariant?为什么数组是不变的,而列表是协变的? 【发布时间】:2011-10-04 19:08:42 【问题描述】:

例如为什么

val list:List[Any] = List[Int](1,2,3)

工作,但是

val arr:Array[Any] = Array[Int](1,2,3)

失败(因为数组是不变的)。这个设计决策背后的预期效果是什么?

【问题讨论】:

注意java数组是协变的,这可能会在scala调用java代码时出现问题。 @incrop - 你能举个例子吗? 【参考方案1】:

因为否则会破坏类型安全。 如果没有,你可以这样做:

val arr:Array[Int] = Array[Int](1,2,3)
val arr2:Array[Any] = arr
arr2(0) = 2.54

编译器无法捕捉到它。

另一方面,列表是不可变的,所以你不能添加不是Int的东西

【讨论】:

你的意思是Array,不是List,是吗?使用列表,您的示例将不起作用(List 类型中没有“更新”方法)。使用Arrays,如果数组协变,这将是您可以做什么的有效反例。 功劳应该归于@sshannin,因为我只是举了一个例子并改写了他所说的话。 为什么arr2(0) 在您的示例@OpDeCirkel 中评估为2.54 @KevinMeredith,这不是评估,而是分配。在示例中,如果 scala 中的数组不是不变的,我们将苹果在 Int 数组中添加浮点数(在上面的示例中为 2.54)。如果数组不是不变的,该示例会突出显示问题。 @OpDeCirkel 我认为值得一提的是另一个常见的不可变集合Set,虽然可以安全地向上转换,但它的类型也是不变的 - 这是因为Set[A] 也是A => Boolean , 但函数的参数是逆变的。 Sets 可以通过 Set[Child]().asInstanceOf[Set[Parent]]Set[Child]().toSet[Parent] 向上转换 - 第一个有气味但不会创建新集合。【参考方案2】:

这是因为列表是不可变的,而数组是可变的。

【讨论】:

功劳应该归于@sshannin,因为我只是举了一个例子并改写了他所说的话。 -1。为什么人们会赞成这样的答案?如果不解释为什么这是相关的,您可能会说“因为Array 以'A' 开头而List 以'L' 开头”。 说得好,特拉维斯。在阅读您的评论之前,我投票赞成它,然后意识到我只是赞成它,因为当我到达这里时我已经知道答案,而这只是简洁地表达了它。但是,只有在您已经知道答案的情况下才有效的答案并不是那么有用。【参考方案3】:

给出的正常答案是可变性与协变相结合会破坏类型安全。对于收藏,这可以作为一个基本事实。但该理论实际上适用于任何泛型类型,而不仅仅是像 ListArray 这样的集合,而且我们根本不需要尝试和推理可变性。

真正的答案与函数类型与子类型交互的方式有关。简而言之,如果将类型参数用作返回类型,则它是协变的。另一方面,如果将类型参数用作实参类型,则它是逆变的。如果它既用作返回类型又用作参数类型,则它是不变的。

让我们看看documentation for Array[T]。两种显而易见的方法是用于查找和更新的方法:

def apply(i: Int): T
def update(i: Int, x: T): Unit

在第一个方法中T 是一个返回类型,而在第二个方法中T 是一个参数类型。因此,方差规则规定T 必须是不变的。

我们可以比较documentation for List[A] 来了解它为什么是协变的。令人困惑的是,我们会发现这些方法类似于Array[T] 的方法:

def apply(n: Int): A
def ::(x: A): List[A]

由于A 既用作返回类型又用作参数类型,我们希望A 是不变的,就像T 用于Array[T] 一样。然而,与Array[T] 不同的是,文档在:: 的类型上对我们撒谎。对于大多数调用此方法的谎言来说,这个谎言已经足够好了,但不足以决定A 的方差。如果我们展开此方法的文档并单击“完整签名”,我们就会看到真相:

def ::[B >: A](x: B): List[B]

所以A 实际上并没有作为参数类型出现。相反,B(可以是A 的任何超类型)是参数类型。这对A 没有任何限制,因此它确实可以是协变的。 List[A] 上的任何以 A 为参数类型的方法都是类似的谎言(我们可以判断,因为这些方法被标记为 [use case])。

【讨论】:

你没有解释为什么“这个谎言对于大多数调用这个方法来说已经足够好了”,你也没有解释为什么他们选择不将这个谎言用于 Array 并使其也是协变的。跨度> 如果我能不止一次 +1...:: 的真实签名部分是无价的 @rapt 从2.8 开始做出决定,让读者从集合 API 中类型注释的细微差别中解脱出来,只显示涵盖绝大多数用例的更温和的签名。这就是为什么它在文档中说...[A] 而不是...[B >: A] onlyonly 以帮助更温和地阅读。这并不意味着实际的方法签名更简单。实际的方法签名仍然是具有正确方差的更复杂的签名。【参考方案4】:

区别在于Lists 是不可变的,而Arrays 是可变的。

要了解可变性决定方差的原因,请考虑制作List 的可变版本——我们称之为MutableList。我们还将使用一些示例类型:一个基类Animal 和两个名为CatDog 的子类。

trait Animal 
  def makeSound: String


class Cat extends Animal 
  def makeSound = "meow"
  def jump = // ...


class Dog extends Animal 
  def makeSound = "bark"

请注意,CatDog 多了一种方法 (jump)。

然后,定义一个接受可变动物列表并修改列表的函数:

def mindlessFunc(xs: MutableList[Animal]) = 
  xs += new Dog()

现在,如果将猫列表传递给函数,将会发生可怕的事情:

val cats = MutableList[Cat](cat1, cat2)
val horror = mindlessFunc(cats)

如果我们使用了粗心的编程语言,编译时会忽略这一点。尽管如此,如果我们只使用以下代码访问猫列表,我们的世界不会崩溃:

cats.foreach(c => c.makeSound)

但如果我们这样做:

cats.foreach(c => c.jump)

将发生运行时错误。使用 Scala,编写这样的代码是被阻止的,因为编译器会抱怨。

【讨论】:

这并没有回答问题,实际上根本没有提到数组。 OP 可能会推断问题来自数组的可变性,但这里没有说明。 @csjacobs24 我在答案顶部添加了一行,以提供对该问题的直接答案。原始答案的目的是解释为什么可变列表协变是错误的。 @YuhuanJiang 不错的答案,最好将其展开以解释当列表不可变(默认)时问题不会发生。同样在答案的开头,您可以开始向不知道liskov原理的人解释协方差到底是什么。对于这个重要问题,这里没有一个答案是全面的。

以上是关于为啥数组是不变的,而列表是协变的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥当“?扩展 Klass”被允许时,泛型被认为是不变的?

scala中数组和列表的区别

scala中数组和列表的区别

在 C# 4.0 中,为啥方法中的 out 参数不能是协变的?

Java 中协变类型擦除与桥接方法

泛型协变与抗变