为什么数组不变,但列出协变?

时间:2011-07-13 19:30:41

标签: arrays list scala covariance

E.g。为什么

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

工作,但

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

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

4 个答案:

答案 0 :(得分:72)

因为它会破坏类型安全性。 如果没有,你可以做这样的事情:

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

并且编译器无法捕获它。

另一方面,列表是不可变的,因此您无法添加不是Int的内容

答案 1 :(得分:26)

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

答案 2 :(得分:5)

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

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

trait Animal {
  def makeSound: String
}

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

class Dog extends Animal {
  def makeSound = "bark"
}

请注意,Cat还有一种方法(jump)而不是Dog

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

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,可以防止编写此类代码,因为编译器会抱怨。

答案 3 :(得分:4)

给出的正常答案是,与协方差相结合的可变性会破坏类型安全性。对于收藏品,这可以作为一个基本的事实。但是这个理论实际上适用于任何泛型类型,而不仅仅是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的方差。如果我们扩展此方法的文档并单击" Full Signature",我们会看到真相:

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

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