我已经阅读了一些教程,包括有关协变类型方法签名的主要 Scala 文档。假设我有以下抽象类:
abstract class List[+A] {
def head: A
def tail: List[A]
def isEmpty: Boolean
def add[B >: A](element: B): List[B]
protected def printElements: String
override def toString: String = "[" + printElements + "]"
}
我的问题涉及add()
方法的签名。为什么有必要这样声明?我们正在传入一个参数,该参数是 A 的超类型。这解决了什么问题?我试图在直观的层面上理解这一点。
正式解释
鉴于
abstract class List[+A] {
def add(element: A): List[A]
}
"这个程序不编译,因为
add
中的参数元素是类型A
,我们声明了协变。这不起作用,因为函数的参数类型是逆变的,结果类型是协变的。为了解决这个问题,我们需要翻转add
中参数元素类型的方差。
我们通过引入一个新的类型参数B
来做到这一点,该参数A
作为下限类型边界"。
--参考。
直观的解释
在此示例中,如果您向Listadd
某些内容:
它必须是A
- 在这种情况下,列表仍然是List[A]
。
或者它必须是A
的任何子类型- 在这种情况下,元素被向上转换为A
,并且列表仍然是List[A]
。
或者,如果它是另一种类型B
,那么它必须是A
的超类型- 在这种情况下,列表被转换为List[B]
。(注意:因为Any
只是一切的超类型,在最坏的情况下,列表将被上调为List[Any]
)。
假设我想列出一个整数列表。并且假设,为了论证,add
是在没有泛型的情况下实现的。
def add(element: A): List[A]
为了这个例子,假设我们有某种方法来生成一个"空"列表。
def emptyList[A]: List[A] = /* some magic */
现在我想制作我的整数列表。
(1 to 10).foldRight(emptyList) { (x, acc) => acc.add(x) }
哎呀!我们有问题!当我调用emptyList
时,Scala将推断出最一般的类型,并且由于A
是协变的,它将假设Nothing
。这意味着我只是尝试将整数添加到无的列表中。我们可以使用显式类型签名来解决此问题,
(1 to 10).foldRight(emptyList[Int]) { (x, acc) => acc.add(x) }
但是,实际上,这并不能解决问题。它不会增加可读性,只需要用户做额外的工作。实际上,我应该能够在无的列表中附加一个数字。只是,如果我选择这样做,我就不能再有意义地称它为Nothing
列表了。因此,如果我们定义
def add[B >: A](element: B): List[B]
现在,我可以从List[Nothing]
开始,并向其添加Int
。我出去的东西不再是List[Nothing]
;这是一个List[Int]
,但我可以做到。如果我拿着这个List[Int]
,稍后再来给它添加一个String
,那么我也可以这样做,但现在我有一个几乎无用的List[Any]
.
当你声明+A
时,你是在说,例如,List[String]
扩展List[Object]
。现在,想象一下:
val ls: List[Object] = List[String]() // Legal because of covariance
ls.add(1) // Adding an int to a list of String?
仅当列表的类型可以扩展为包含任意对象时,这才是合法的,这正是您的添加签名所做的。否则,add(a: A)
的存在将意味着类型系统中的不一致。