为什么数组不变,但列表协变?
为什么呢
val list:List[Any] = List[Int](1,2,3)
工作,但是
val arr:Array[Any] = Array[Int](1,2,3)
失败(因为数组是不变的)。 这个devise决定背后的预期效果是什么?
因为否则会打破型号安全。 如果没有,你可以做这样的事情:
val arr:Array[Int] = Array[Int](1,2,3) val arr2:Array[Any] = arr arr2(0) = 2.54
编译器不能捕捉它。
另一方面,列表是不可变的,所以你不能添加一些不是Int
东西
这是因为列表是不可变的,数组是可变的。
区别在于List
是不可变的,而Array
是可变的。
要理解为什么可变性决定了方差,可以考虑制作一个可变版本的List
– 我们称之为MutableList
。 我们还将使用一些示例types:一个基类Animal
和两个名为Cat
和Dog
子类。
trait Animal { def makeSound: String } class Cat extends Animal { def makeSound = "meow" def jump = // ... } class Dog extends Animal { def makeSound = "bark" }
请注意, Cat
比Dog
有更多的方法( 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中,编写这样的代码是被阻止的,因为编译器会抱怨。
给出的正常答案是可变性与协方差相结合会破坏types安全性。 对于collections,这可以被视为一个基本事实。 但是这个理论实际上适用于任何genericstypes,而不仅仅是像List
和Array
这样的集合。
真正的答案与函数types与子types相互作用的方式有关。 简言之,如果一个types参数被用作返回types,它是协变的。 另一方面,如果一个types参数被用作参数types,它是逆变的。 如果它既用作返回types又用作参数types,则它是不变的。
我们来看一下Array[T]
的文档 。 两种显而易见的方法是查找和更新:
def apply(i: Int): T def update(i: Int, x: T): Unit
在第一个方法中, T
是一个返回types,而在第二个T
是一个参数types。 变异规则决定了T
必须是不变的。
我们可以比较List[A]
的文档,看看它为什么是协变的。 令人困惑的是,我们会发现这些方法,类似于Array[T]
的方法:
def apply(n: Int): A def ::(x: A): List[A]
由于A
被用作返回types和参数types,所以我们期望A
是不变的,就像T
是Array[T]
。 但是,与Array[T]
不同的是,文档向我们讲述了::
的types。 对于大多数这种方法的调用来说,这个谎言已经足够了,但是不足以决定A
的方差。 如果我们扩展这个方法的文档,点击“Full Signature”,我们就会看到这个事实:
def ::[B >: A](x: B): List[B]
所以A
实际上并不是一个参数types。 相反, B
(可以是A
任何超types)是参数types。 这对A
没有任何限制,所以它可以是协变的。 任何具有A
作为参数types的List[A]
上的方法都是类似的谎言(我们可以看出,因为这些方法被标记为[use case]
)。