为什么数组是不变的,但列表是协变的?

57

例如,为什么

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

工作,但是

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

因为数组是不变的,所以它失败了。这个设计决策的期望效果是什么?


8
请注意,Java 数组是协变的,这可能在从 Scala 调用 Java 代码时会导致问题。 - incrop
@incrop - 你能举个例子吗? - Kevin Meredith
@KevinMeredith 整数[] source = {1, 2, 3}; 数字[] target = source; target [0] = 4; target [0] = 3.14; //编译通过//但在运行时会发生ArrayStoreException//堆污染尝试 - bridgemnc
4个回答

80

否则会破坏类型安全。如果不这样,你将能够执行这样的操作:

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

而编译器无法捕获它。

另一方面,列表是不可变的,因此您不能添加不是Int类型的内容。


1
你是指 Array,而不是 List,对吗?使用列表,你的示例将无法工作(类型 List 中没有“update”方法)。如果数组是协变的,那么它将是一个有效的反例,说明你可以做什么。 - Dirk
1
应该归功于@sshannin,因为我只是举了一个例子并重新表述了他说的话。 - Op De Cirkel
在您的示例中,为什么arr2(0)计算结果为2.54,@OpDeCirkel? - Kevin Meredith
@KevinMeredith,这不是评估,而是任务。在这个例子中,如果Scala中的数组不是不变的,我们就可以在Int数组中添加浮点数(例如上面的2.54)。该示例突出了如果数组不是不变的问题。 - Prashant Kalkar
@OpDeCirkel 我认为值得一提的是,另一个常见的不可变集合 Set,虽然可以安全地向上转型,但在其类型上也是不变的 - 这是因为 Set[A] 也是一个 A => Boolean,但函数在其参数上是逆变的。Set 可以通过 Set[Child]().asInstanceOf[Set[Parent]]Set[Child]().toSet[Parent] 进行向上转型 - 第一个方法不太好看,但不会创建新的集合。 - Sergey
在Coursera的“Scala函数式编程原理”课程中,有一节讲座专门探讨这个问题: 第4周 - 第4.4节 - 方差(可选)这个链接可能不会永久有效,但如果有人很快查看这个问题: https://es.coursera.org/learn/progfun1/lecture/dnreZ/lecture-4-4-variance-optional - dieresys

25

这是因为列表(lists)是不可变(immutable)的,而数组(arrays)是可变(mutable)的。


3
应该归功于 @sshannin,因为我只是举了一个例子并重新表述了他所说的话。 - Op De Cirkel
23
为什么人们会点赞这样的回答?如果没有对其相关性进行解释,那么你说“因为Array以'A'开头,而List以'L'开头”也同样无意义。 - Travis Brown
1
这是个好观点,Travis。在阅读你的评论之前我已经点赞了,但后来意识到我只是因为我已经知道答案并且这篇回答简明地表达了出来。但如果答案只有在你已经知道它的情况下才有效,那么这样的答案并不是很有用。 - csjacobs24

9
一般来说,可变性与协变性相结合会破坏类型安全性。对于集合而言,这可以被视为一个基本真理。但实际上,这个理论适用于任何泛型类型,而不仅仅是像 ListArray 这样的集合,并且我们根本不必考虑可变性。
真正的答案与函数类型与子类型之间的交互方式有关。简而言之,如果类型参数用作返回类型,则它是协变的。另一方面,如果类型参数用作参数类型,则它是逆变的。如果它既用作返回类型又用作参数类型,则它是不变的。
让我们看看 Array[T] 的文档。需要关注的两种明显的方法是查找和更新方法:
def apply(i: Int): T
def update(i: Int, x: T): Unit

在第一种方法中,T 是返回类型,而在第二种方法中,T 是参数类型。协变的规则指定 T 必须是不变的。
我们可以比较 List[A] 的文档 来看为什么它是协变的。令人困惑的是,我们会发现这些方法与 Array[T] 的方法类似:
def apply(n: Int): A
def ::(x: A): List[A]

由于A既用作返回类型又用作参数类型,我们期望AArray[T]中的T一样是不变的。然而,与Array[T]不同的是,文档在关于::的类型上是在撒谎。这个谎言对于大多数对该方法的调用来说已经足够了,但无法足以决定A的变异性。如果我们扩展此方法的文档并单击“完整签名”,我们将看到实际情况:

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

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

1
你没有解释为什么“这个谎言足以应对大多数调用该方法的情况”,也没有解释为什么他们选择不在数组中使用这个谎言,使其成为协变。 - rapt
1
如果我可以多次点赞+1...关于::的真实签名部分是无价的。 - Ashkan Kh. Nazary
@rapt 从 2.8 开始,决定让读者免于在集合 API 中处理类型注释的微妙之处,只显示涵盖绝大多数用例的更温和的签名。这就是为什么文档中说 ...[A] 而不是 ...[B >: A]仅仅作为更易读的辅助说明。这并不意味着实际的方法签名是更简单的那个。实际的方法签名仍然是具有正确变化性的更复杂的那个。 - Ashkan Kh. Nazary

7

区别在于List是不可变的,而Array是可变的。

要理解为什么可变性决定了方差,考虑创建一个可变版本的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中,编译器会提示,从而避免这种情况的发生。


这并没有回答问题,实际上根本没有提到数组。原帖的作者可能会推断问题来自于数组的可变性,但这里并没有说明。 - csjacobs24
1
@csjacobs24 我在答案顶部添加了一行,直接回答了问题。原始答案的目的是解释为什么可变列表不能协变。 - Yuhuan Jiang
@YuhuanJiang,回答很好,如果您能扩展解释当列表是不可变时(默认情况下)问题不会发生的原因,那就更好了。此外,在回答开头,您可以先解释一下协变实际上是什么,以及Liskov原则是什么,这对于不了解的人来说可能会更清楚。这个重要问题的所有答案都不够全面。 - Rpant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接