5.7 参数化类型的可变性
至此,我们已经见识过许多Scala的惯用法,但是还有一件事,我想在这章的最后介绍一下。也许,本节会让你觉得有点头痛,但是我相信你可以搞定的。好,让我们系紧安全带,以防发生意外!
我们见识过,Scala会如何阻止可能引发运行时失败的赋值。例如,以下代码无法通过编译:
var arr1 = new Array[Int](3)
var arr2: Array[Any] = null
arr2 = arr1 // Compilation ERROR
上面的约束是件好事。想象一下,假如Scala——跟Java一样——不这么限制会出现什么结果。下面是一段会让我们陷入麻烦的Java代码:
ScalaIdioms/Trouble.java
Line 1 //Java code
- class Fruit {}
- class Banana extends Fruit {}
- class Apple extends Fruit {}
5
- public class Trouble {
- public static void main(String[] args) {
- Banana[] basketOfBanana = new Banana[2];
- basketOfBanana[0] = new Banana();
10
- Object[] basketOfFruits = basketOfBanana; // Trouble
- basketOfFruits[1] = new Apple();
-
- for(Banana banana : basketOfBanana) {
15 System.out.println(banana);
- }
- }
- }
上面这段代码没有编译错误。不过,运行它,会报出如下的运行时错误:
Exception in thread "main" java.lang.ArrayStoreException: Apple
at Trouble.main(Trouble.java:12)
让我们公平一些,其实Java也是不允许下面这种做法的:
//Java code
ArrayList<Integer> list = new ArrayList<Integer>();
ArrayList<Object> list2 = list; // Compilation error
不过,在Java里很容易绕开这点像这样:
ArrayList list3 = list;
将子类实例的容器赋给基类容器的能力称为协变(covariance)。将超类实例的容器赋给子类容器的能力称为逆变(contravariance)。默认情况下,Scala对二者都不支持。
虽然Scala的默认行为总的来说是好的,但是在某些实际情况下,我们希望能够慎重地把派生类型的容器(比如Dog
类型的容器)当作它的基类型的容器(比如pet
的容器)。考虑下面的例子:
ScalaIdioms/PlayWithPets.scala
class Pet(val name: String) {
override def toString() = name
}
class Dog(override val name: String) extends Pet(name)
def workWithPets(pets: Array[Pet]) {}
这里定义了两个类——Pet
类,以及继承自它的Dog
类。还有一个方法workWith-Pets()
,接收一个Pet
数组,其实什么都没做。现在,创建一个Dog
数组:
ScalaIdioms/PlayWithPets.scala
val dogs = Array(new Dog("Rover"), new Dog("Comet"))
如果把dogs
传给上面的方法,会报出编译错误:
workWithPets(dogs) // Compilation ERROR
调用workWithPets()
时,Scala会提示错误——Dog
数组不能传给接收Pet
数组的方法。但是,对这个方法而言,这么做没有任何不良后果,对吧?不过,Scala不知道这一点,它在试图保护我们。我们需要告诉 Scala,这是可行的,可以这么做。下面这个例子告诉我们如何做到这一点:
ScalaIdioms/PlayWithPets.scala
def playWithPets[T <: Pet](pets: Array[T]) =
println("Playing with pets: " + pets.mkString(", "))
这里用特殊的语法定义了playWithPets()
。T<:Pet
表示T所代表的类派生自Pet
。通过使用这种有上界的语法⑥,我们告诉Scala,具有参数化类型T的参数数组必须至少是Pet
的数组,也可以是任何Pet
派生类的数组。这样一来,就可以进行这样的调用了:
⑥如果将对象层次结构可视化,可以看到
Pet
定义了类型T
的上界,T
可以是Pet
或是层次结构中任意更低的类型。ScalaIdioms/PlayWithPets.scala
playWithPets(dogs)
对应的输出如下:
Playing with pets: Rover, Comet
如果试图传入一个Object
数组或是其他非派生自Pet类型的对象数组,就会报出编译错误。
现在,我们想复制pet
。编写了一个名为copy()
的方法,接收两个参数,类型是Array[Pet]
。不过,这样依然不能传入Dog
数组,但我们也知道,Dog
数组是应该可以复制给Pet
数组的,换句话说,接收的数组可以是一个容器,其元素类型是源数组元素类型的超类型。所以,这里我们需要的是一个下界:
ScalaIdioms/PlayWithPets.scala
def copyPets[S, D >: S](fromPets: Array[S], toPets: Array[D]) = { //...
}
val pets = new Array[Pet](10)
copyPets(dogs, pets)
将目的数组的参数化类型(D
)限制为源数组的参数化类型(S
)的超类型。换句话说,S
(源类型,如Dog
)设置了类型D
(目的类型,如Dog
或Pet
)的下界——它可以是类型S
或其超类的任意类型。
在上面两个例子里,在方法定义里控制了方法的参数。如果你是容器的作者,你也可以控制这一行为——也就是说,如果你愿意的话,把派生类的容器当作基类的容器,也是可行的。要做到这一点,可以将参数化类型标记为+T
,而不是T
,就像下面这个例子一样:
ScalaIdioms/MyList.scala
class MyList[+T] //...
var list1 = new MyList[int]
var list2 : MyList[Any] = null
list2 = list1 // OK
这里,+T
告诉Scala允许协变;换句话说,在类型检查期间,让Scala接收某个类型或者其基类型。这样,就可以将MyList[Int]
赋给MyList[Any]
。记住,对于Array[Int]
而言,这是不可以的。不过,对于List
——Scala程序库中实现的函数式list
——而言,这是可以的。我们会在第8章“使用容器”讨论这些内容。
类似的,参数化类型用-T
替换T
,就可以让Scala支持类型逆变。
默认情况下,Scala编译器会严格限制类型变化。你也见识了如何请求更为宽松的协变或逆变。在任何情况下,Scala编译器都会根据类型变化的标记检查类型正确性,然后把结果告诉我们。
在本章里,我们讨论了Scala的静态类型及其类型推演,见识了如何通过它使代码简洁。在下一章中,带着对类型、类型推演和如何编写方法的理解,让我们准备学习和享受Scala带来的更多简洁概念。