E.g。为什么
val list:List[Any] = List[Int](1,2,3)
工作,但
val arr:Array[Any] = Array[Int](1,2,3)
失败(因为数组是不变的)。这个设计决定背后的效果是什么?
答案 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个名为Cat
和Dog
的子类。
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)
给出的正常答案是,与协方差相结合的可变性会破坏类型安全性。对于收藏品,这可以作为一个基本的事实。但是这个理论实际上适用于任何泛型类型,而不仅仅是List
和Array
这样的集合,我们根本不必尝试和推理可变性。
真正的答案与函数类型与子类型交互的方式有关。简短的故事是,如果将类型参数用作返回类型,则它是协变的。另一方面,如果将类型参数用作参数类型,则它是逆变的。如果它既用作返回类型又用作参数类型,则它是不变的。
让我们看一下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]
)。