Scala学习笔记 之 类型参数(泛型)

一个简单的例子

类型参数的基本使用很好理解,来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Queue[T] private ( // A private constructor
private val leading: List[T],
private val trailing: List[T]) {

private def mirror =
if (leading.isEmpty)
new Queue[T](trailing.reverse, Nil)
else
this

def isEmpty = mirror.leading == Nil

def head = mirror.leading.head

def tail = {
val q = mirror
new Queue[T](q.leading.tail, q.trailing)
}

def enqueue(elem: T) = new Queue[T](leading, elem :: trailing)

override def toString = {
val s = if (trailing == Nil) "" else ", " + trailing.reverse.mkString(", ")
"Queue[" + leading.mkString(", ") + s + "]"
}

override def equals(o: Any) = o match {
case q: Queue[T] => //this.toString == q.toString
if (isEmpty) q.isEmpty
else if (q.isEmpty) isEmpty
else head == q.head && tail == q.tail
case _ => false
}
}

object Queue {
/**
* Factory method for constructing Queue
*/
def apply[T](elems: T*) = new Queue[T](elems.toList, Nil)
}

这是一个完全无状态的队列的实现。其元素的类型是不确定的,所以使用了类型参数。

类型参数的使用看似简单,但是其实会引出一系列的问题:

协变、逆变和不变的类型参数

这里衍生出来的一个问题就是:如果TS是子类型,那么Queue[T]Queue[S]的子类型吗?或者Queue[T]Queue[S]是什么关系呢?

这里就涉及到类型参数的一个性质,被称为variance(不知道怎么翻译)。在这里给出一个定义:

如果TS的子类型,那么:
协变的(covariant):(Queue[+T])Queue[T]是Queue[S]的子类型。
不变的(nonvariant)(默认)Queue[T]和Queue[S]之间没有继承关系。
逆变的(contravariant)(Queue[-T])Queue[S]是Queue[T]的子类型。

在默认的情况下,Scala中的类型参数都是不变的

如果想要定义一个协变的类型参数,那么一定要保证这个类的对象是无状态的(函数式的)。如果这个类中有var字段(即是有状态的),那么就可以会出问题。看如下的例子。

1
2
3
4
5
class Cell[+T](init: T) {
private[this] var current = init
def get = current
def set(x: T) { current = x }
}

这里定义了一个Cell类,它是一个有状态类,具有协变的类型参数。如果执行以下的代码:

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

表面上看没有任何问题。但是仔细观察发现,c1是Cell[String]类型,也就是说其get方法应该返回一个String,但是实际上其current字段已经被修改成Int了。

实际上,上面的代码在编译时就会报错的:

1
2
3
4
Cell.scala:7: error: covariant type T occurs in
contravariant position in type T of value x
def set(x: T) = current = x
ˆ

在这里需要提到,Java中的数组是协变类型的,可就是说String[]可以被看作一个Object[];而在Scala中数组的参数类型则是不变的:

1
2
3
4
5
6
7
8
scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)
scala> val a2: Array[Any] = a1
<console>:5: error: type mismatch;
found : Array[java.lang.String]
required: Array[Any]
val a2: Array[Any] = a1
ˆ

但是因为JVM中的数组是协变处理的,所以在Scala中也可以显式进行转置:

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

这样的转置不会有编译问题,但在如果操作不当,运行时可能会出现ArrayStore异常。