为啥数组是不变的,而列表是协变的?
Posted
技术标签:
【中文标题】为啥数组是不变的,而列表是协变的?【英文标题】:Why are Arrays invariant, but Lists covariant?为什么数组是不变的,而列表是协变的? 【发布时间】:2011-10-04 19:08:42 【问题描述】:例如为什么
val list:List[Any] = List[Int](1,2,3)
工作,但是
val arr:Array[Any] = Array[Int](1,2,3)
失败(因为数组是不变的)。这个设计决策背后的预期效果是什么?
【问题讨论】:
注意java数组是协变的,这可能会在scala调用java代码时出现问题。 @incrop - 你能举个例子吗? 【参考方案1】:因为否则会破坏类型安全。 如果没有,你可以这样做:
val arr:Array[Int] = Array[Int](1,2,3)
val arr2:Array[Any] = arr
arr2(0) = 2.54
编译器无法捕捉到它。
另一方面,列表是不可变的,所以你不能添加不是Int
的东西
【讨论】:
你的意思是Array
,不是List
,是吗?使用列表,您的示例将不起作用(List
类型中没有“更新”方法)。使用Array
s,如果数组协变,这将是您可以做什么的有效反例。
功劳应该归于@sshannin,因为我只是举了一个例子并改写了他所说的话。
为什么arr2(0)
在您的示例@OpDeCirkel 中评估为2.54
?
@KevinMeredith,这不是评估,而是分配。在示例中,如果 scala 中的数组不是不变的,我们将苹果在 Int 数组中添加浮点数(在上面的示例中为 2.54)。如果数组不是不变的,该示例会突出显示问题。
@OpDeCirkel 我认为值得一提的是另一个常见的不可变集合Set
,虽然可以安全地向上转换,但它的类型也是不变的 - 这是因为Set[A]
也是A => Boolean
, 但函数的参数是逆变的。 Set
s 可以通过 Set[Child]().asInstanceOf[Set[Parent]]
或 Set[Child]().toSet[Parent]
向上转换 - 第一个有气味但不会创建新集合。【参考方案2】:
这是因为列表是不可变的,而数组是可变的。
【讨论】:
功劳应该归于@sshannin,因为我只是举了一个例子并改写了他所说的话。 -1。为什么人们会赞成这样的答案?如果不解释为什么这是相关的,您可能会说“因为Array
以'A' 开头而List
以'L' 开头”。
说得好,特拉维斯。在阅读您的评论之前,我投票赞成它,然后意识到我只是赞成它,因为当我到达这里时我已经知道答案,而这只是简洁地表达了它。但是,只有在您已经知道答案的情况下才有效的答案并不是那么有用。【参考方案3】:
给出的正常答案是可变性与协变相结合会破坏类型安全。对于收藏,这可以作为一个基本事实。但该理论实际上适用于任何泛型类型,而不仅仅是像 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
的方差。如果我们展开此方法的文档并单击“完整签名”,我们就会看到真相:
def ::[B >: A](x: B): List[B]
所以A
实际上并没有作为参数类型出现。相反,B
(可以是A
的任何超类型)是参数类型。这对A
没有任何限制,因此它确实可以是协变的。 List[A]
上的任何以 A
为参数类型的方法都是类似的谎言(我们可以判断,因为这些方法被标记为 [use case]
)。
【讨论】:
你没有解释为什么“这个谎言对于大多数调用这个方法来说已经足够好了”,你也没有解释为什么他们选择不将这个谎言用于 Array 并使其也是协变的。跨度> 如果我能不止一次 +1...::
的真实签名部分是无价的
@rapt 从2.8
开始做出决定,让读者从集合 API 中类型注释的细微差别中解脱出来,只显示涵盖绝大多数用例的更温和的签名。这就是为什么它在文档中说...[A]
而不是...[B >: A]
only 和 only 以帮助更温和地阅读。这并不意味着实际的方法签名更简单。实际的方法签名仍然是具有正确方差的更复杂的签名。【参考方案4】:
区别在于List
s 是不可变的,而Array
s 是可变的。
要了解可变性决定方差的原因,请考虑制作List
的可变版本——我们称之为MutableList
。我们还将使用一些示例类型:一个基类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,编写这样的代码是被阻止的,因为编译器会抱怨。
【讨论】:
这并没有回答问题,实际上根本没有提到数组。 OP 可能会推断问题来自数组的可变性,但这里没有说明。 @csjacobs24 我在答案顶部添加了一行,以提供对该问题的直接答案。原始答案的目的是解释为什么可变列表协变是错误的。 @YuhuanJiang 不错的答案,最好将其展开以解释当列表不可变(默认)时问题不会发生。同样在答案的开头,您可以开始向不知道liskov原理的人解释协方差到底是什么。对于这个重要问题,这里没有一个答案是全面的。以上是关于为啥数组是不变的,而列表是协变的?的主要内容,如果未能解决你的问题,请参考以下文章