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

Posted

技术标签:

【中文标题】直观地解释为啥`List`是协变的,而`Array`是不变的?【英文标题】:Intuitively explain why `List` is covariant but `Array` is invariant?直观地解释为什么`List`是协变的,而`Array`是不变的? 【发布时间】:2021-03-18 21:18:38 【问题描述】:

来自List[+T],我了解到狗的列表也是与直觉完全一致的动物列表。从def :: [B >: A](elem: B): List[B] 我知道我可以将动物(B,不太具体)添加到狗列表(A,更具体),并会返回动物列表。这也符合直觉。所以基本上List 是好的。

来自Array[T] 我了解一组狗不是(不能用来代替a)一组动物,这是相当违反直觉的。一组狗确实也是一组动物,但显然 Scala 不同意。

我希望有人能直观地解释为什么 Array 保持不变,最好是用狗(或猫)来解释。

有Why are Arrays invariant, but Lists covariant?,但我正在寻找一个不(大量)涉及类型系统的更直观的解释。

与Why is Scala's immutable Set not covariant in its type?相关

【问题讨论】:

我自己的预感是“一个Array 的狗确实是一个Array 在动物身上”可能并不适用于所有情况,特别是当它不用于只读位置但我不能直观地说出来。 AFAIK 原因几乎是历史性的——当 Java 引入带有协变数组的泛型时,人们意识到它们可能会爆炸(记住,它们是可变的)。例如。您可以简单地获取一个苹果数组,将其分配给 Array[Fruit] 类型的变量,然后将香蕉放入其中。这在运行时抛出。这就是为什么像数组这样的可变东西最好保持不变,而像 scala List 这样的不可变集合可以(并且确实有用)是协变的。 @slouc: "当 Java 引入带有协变数组的泛型时,人们意识到它们可能会爆炸" – 1) Java 中的协变数组独立于泛型而被破坏。它们早在 Java 1 中就已经被破坏了Java 5 中的泛型。 2) 关于安全协变和逆变的规则早在 Java 引入协变数组之前就已为人所知。如果人们在 Java 引入它们时才意识到它们可能会爆炸,那么他们要么无知,要么愚蠢。如果你看看那些设计 Java 的人的名字,很明显他们都不是那些东西,而是他们创造了一个…… ...为了程序员的方便而牺牲安全性的设计决策,知道完全协变数组是不安全的 【参考方案1】:

原因很简单。是因为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 根本不允许您编译。

【讨论】:

以上是关于直观地解释为啥`List`是协变的,而`Array`是不变的?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 4.0 中,为啥方法中的 out 参数不能是协变的?

scala中数组和列表的区别

Java 中协变类型擦除与桥接方法

scala中数组和列表的区别

18.scala的型变

C#中的协变与逆变