Scala 专题教程-参数化类型(3): Variance(类型变体)标识

jerry Scala 2015年11月25日 收藏

在上篇例子中定义的Queue是一个Trait,而不是一个类型,这是因为Queue需要一个类型参数才能构成一个类型, 也就是说你不可以直接创建一个类型为Queue的对象:

scala> def doesNotCompile(q:Queue) {}
<console>:8: error: trait Queue takes type parameters
       def doesNotCompile(q:Queue) {}
                            ^

实际上,Queue允许你指明类型参数,比如Queue[String],Queue[Int]或Queue[AnyRef]等,因此Queue也可以称为一个类型构造器,因为你可以指明一个参数类型后构造一个新的类型。
Queue也可以称为一个通用Trait(包含类型参数的类或Trait称为“generic?) ,而指明了类型参数之后就不再是通用类型,而是特定的类型了,比如Queue[String]等。
类型参数和派生结合起来之后,就会产生一些有趣的问题,比如String是AnyRef 的子类,那么Queue[String]和Queue[AnyRef]之间会不会有什么继承关系呢?
在Scala中,可以有三种不同的关系,缺省情况比如Queue[T],Queue[String]和Queue[AnyRef]不存在继承关系。此外Scala还支持两种关系:Covariance(协变关系)和Contravariance(逆变关系),分别以
Queue[+T]和Queue[-T] 代表。
Covariance(协变关系)是在类型前面使用“+”号,表示如果两个类型T,S 如果T是S的子类,那么Queue[T]也是Queue[S]的子类
而Contravariable(逆变关系)则相反,如果如果两个类型T,S 如果T是S的子类,反过来,Queue[S]是Queue[T]的子类。
我们以一个具体的例子来说明一下,比较直观,定义三个类 GrandParent ,Parent, Child :

class GrandParent 
class Parent extends GrandParent
class Child extends Parent

class Box[+A]
class Box2[-A]

def foo(x : Box[Parent]) : Box[Parent] = identity(x)
def bar(x : Box2[Parent]) : Box2[Parent] = identity(x)

那么我使用下面的几种调用方法来看看+A和-A的不同之处:

scala> foo(new Box[Child]) // success
res1: Box[Parent] = Box@5da444e4

scala> foo(new Box[GrandParent]) // type error
<console>:12: error: type mismatch;
 found   : Box[GrandParent]
 required: Box[Parent]
              foo(new Box[GrandParent]) // type error
                  ^

scala> bar(new Box2[Child]) // type error
<console>:13: error: type mismatch;
 found   : Box2[Child]
 required: Box2[Parent]
              bar(new Box2[Child]) // type error
                  ^

scala> bar(new Box2[GrandParent]) // success
res4: Box2[Parent] = Box2@59615389

[T],[+T],[-T]为类型参数的三种不同的变体。使用+,-称为类型的变体标识。
在纯函数编程的世界中,很多类型存在非常明显的协变关系,但是一但出现可变的数据时,情况就发生了变化,比如我们看看下面的例子:

class Cell[T](init:T) {
	private[this] var current = init
	def get = current
	def set(x:T) { current = x}
}

如果我们假定我们定义的是Cell[+T]而不是上面例子中的Cell[T](实际上编译器会在使用Cell[+T]时报错,我们啦看看为什么?假定我们使用的是Cell[+T]来定义,那么我们可以写如下代码:

val c1 = new Cell[String]("abc")
val c2: Cell[Any] = c1
c2.set(1)
val s:String = c1.get

这四行代码看起来都没有错(假定我们使用的是Cell[+T],那么Cell[String] 是Cell[AnyRef]的子类,因此 c2 可以使用 c1赋值)。最后的结果我们是把整数1赋值给了这字符串类型,这就造成了类型不匹配,这也是为什么编译器在使用Cell[+T]会报错:

<console>:10: error: covariant type T occurs in contravariant position in type T of value x
       def set(x:T) { current = x}

要注意的是在Scala中,Array[T]不是协变关系的,因此Array[String]不是Array[Any]的子类。因此不可以直接把Array[String]类型的变量赋值给Array[Any]类型的变量,例如:

scala> val a1=Array("abc")
a1: Array[String] = Array(abc)

scala> val a2:Array[Any] = a1
<console>:8: error: type mismatch;
 found   : Array[String]
 required: Array[Any]
Note: String <: Any, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)
       val a2:Array[Any] = a1

但是有时需要这种赋值,Scala允许你把特殊类型的数组强制转换成其父类型的数组,例如:

scala> val a2 :Array[Object] = a1.asInstanceOf[Array[Object]]
a2: Array[Object] = Array(abc)