直观地解释为什么`List`是协变的,而`Array`是不变的?

4
List[+T],我理解到:一组狗的列表也是动物的列表,这与直觉完全符合。从def :: [B >: A](elem: B): List[B] 我理解到:我可以将一个动物(B,不太具体)添加到一组狗(A,更具体)的列表中,并得到一组动物的列表。这也符合直觉。所以基本上List还不错。
Array[T],我理解到:狗的数组不是(不能用作)动物数组,这相当反直觉。狗的数组确实也是动物的数组,但Scala显然不同意。
我希望有人能够直观地解释为什么Array是不变的,最好是用狗(或猫)来解释。
Why are Arrays invariant, but Lists covariant?,但我正在寻找一个更直观的解释,不涉及(很重的)类型系统。
相关:Why is Scala's immutable Set not covariant in its type?

1
我个人的直觉是,“一组狗实际上是一组动物”可能并不适用于所有情况,特别是当它不是在只读位置使用时,但我无法直观地表达出来。 - Ashkan Kh. Nazary
2
据我所知,原因基本上都是历史原因 — 当Java引入具有协变数组的泛型时,人们意识到它们可能会爆炸(记住,它们是可变的)。例如,您可以简单地将一个苹果数组分配给类型为Array [Fruit]的变量,然后在其中放置香蕉。这会在运行时抛出异常。因此,像数组这样的可变东西最好保持不变,而像Scala List这样的不可变集合则可以是协变的(确实很有用)。 - slouc
1
@slouc:“当Java引入具有协变数组的泛型时,人们意识到它们可能会爆炸”- 1)无论是否使用泛型,Java中的协变数组都是有问题的。在Java 5中引入泛型之前,它们已经在Java 1中出现了问题。2)关于安全的协变和逆变规则早在Java引入协变数组之前就已经被知晓。如果人们只有在Java引入它们时才意识到它们可能会爆炸,那么他们要么是无知的,要么是愚蠢的。如果你看一下设计Java的人的名字,就应该很清楚他们既不是这些东西,而是做了一个…… - Jörg W Mittag
在设计中,为了方便程序员而牺牲安全性的决策,明知协变数组是不安全的 - Jörg W Mittag
1个回答

18

原因非常简单。因为Array是一个可变的集合。还记得有一个关于方差的非常简单的经验法则吗?
如果它产生了某些东西,它可以是协变的
如果它消耗了某些东西,它可以是逆变的
这就是为什么Functions在输入上是逆变的,在输出上是协变的

因为Arrays可变的,它们实际上既是生产者又是消费者,所以它们必须是不变的

让我用一个简单的例子来说明为什么必须是这样的。

// Assume this compiles, it doesn't.
final class CovariantArray[+A] (arr: Array[A]) {
  def length: Int = arr.length
  def apply(i: Int): A = arr(i)
  def update(i: Int, a: A): Unit = {
    arr(i) = a
  }
}

sealed trait Pet
final case class Dog(name: String) extends Pet
final case class Cat(name: String) extends Pet

val myDogs: CovariantArray[Dog] = CovariantArray(Dog("Luna"), Dog("Lucas"))
val myPets: CovariantArray[Pet] = myDogs // Valid due covariance.
val myCat: Cat = Cat("Milton")
myPets(1) = myCat // Valid because Liskov.
val myDog: Dog = myDogs(1) // Runtime error Cat is not Dog.

你可以使用普通的ArraysJava中重现这个错误,但是Scala将不允许你编译。


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