参数限定(Parameter bounds)考虑这样一个方法:updateMax,他将一个cell的值设置为其当前值与一个给定值之间较大的那个。我们希望这个函数能够作用于所有的cell类型,只要其值类型能够按照一个特征(trait)Ordered定义的“<”操作符进行比较。目前假定这个特征定义如下:(更精确的定义在Scala标准类库中)
trait Ordered[T] {
def < (x: T): boolean
}
这样,updateMax方法可以通过如下方式进行泛型定义,其中使用到的方法称为限定多态(Bounded polymorphism):
def updateMax[T <: Ordered[T]](c: GenCell[T], x: T) =
if (c.get < x) c.set(x)
这里,类型参数定义子句[T <: Ordered[T]]引入了受限类型参数T,它限定参数类型T必须是Ordered[T]的子类型。这样,“<”操作符就可以应用于类型为T的参数了。同时,这个例子还展现出一个受限参数类型本身可以作为其限定类型的一部分,也就是说Scala支持F-受限多态(F-bounded polymorphism[10])。
协变性(Variance)泛型和子类型(subtyping)组合在一起产生这样一个问题:它们如何相互作用。如果C是一个类型构造子(type constructor),S是T的一个子类,那么C[S]是不是也是C[T]的子类呢?我们把有这种特性的类型构造子称为协变的(covariant)。可以看出GenCell这个类型构造子显然不是协变的,否则的话,下面这段代码就是合法的,但实际上它将会在运行时抛出错误:
val x: GenCell[String] = new GenCell[String]("abc")
val y: GenCell[Any] = x; // illegal!
y.set(1)
val z: String = y.get
GenCell中的可变(mutable)变量使其无法成为协变的。实际上,GenCell[String]不是GenCell[Any]的子类,因为有些可以针对GenCell[Any]的操作不能应用于GenCell[String],例如将其设置一个整型值。
另一方面,对于不可变数据类型,构造子的协变性是很自然成立的。例如:一个不可变的整数列表自然可以被看做是一个Any列表的特例。此外,在另一些情况下我们正好需要逆协变性(contravariance),例如一个输出管道Chan[T],有一个以T为类型参数的写操作,我们自然希望对于所有T<:S,都有Chan[S]<:Chan[T]。
Scala允许通过“+/-”定义类型参数的协变性,用“+”放在类型参数前表示构造子对于该参数是协变的,“-”则表示逆协变,没有任何符号则表示非协变。
下面的GenList定义了一个协变的列表,包含isEmpty、head和tail等三个方法。
abstract class GenList[+T] {
def isEmpty: boolean
def head: T
def tail: GenList[T]
}
Scala的类型系统通过跟踪类型参数的每一次使用来确保协变性确实成立。这些使用位置被分为几类:出现在不可变字段和方法返回结果被认为是协变的;出现在方法参数和类型参数的上/下界时被认为是逆协变的;非协变的类型参数永远出现在非协变的位置;在一个逆协变类型参数的内部,协变与逆协变是反转的。类型系统保证协变(逆协变)的类型参数总是出现在协变(逆协变)的位置上。(这段话叙述比较抽象,没有给出任何例子。由于是在说明Scala编译器在协变这个概念上的实现机制,不关心语言实现细节的话可以在一定程度上忽略——译注)
下面是GenList的两个实现:
object Empty extends GenList[Nothing] {
def isEmpty: boolean = true
def head: Nothing = throw new Error("Empty.head")
def tail: GenList[Nothing] = throw new Error("Empty.tail")
}
class Cons[+T](x: T, xs: GenList[T])
extends GenList[T] {
def isEmpty: boolean = false
def head: T = x
def tail: GenList[T] = xs
}
注意:Empty对象代表一个空列表,其元素可以是任何类型。这一点就是由协变性保证的,因为Empty的类型是GenList[Nothing],对于任何T而言,它都是GenList[T]的子类型。