型变是复杂类型的子类型关系与它们组件的类型的子类型关系之间的关联性。Scala支持 泛型类的类型参数上的型变注解,可以使得它们是协变的、逆变的或者不变的(如果没有使用注解的话)。型变在类型系统中的使用使得我们在复杂的类型之间建立起了直观的联系,反之如果没有型变则限制了抽象类的重用。
1 | class Foo[+A] // 协变类 |
协变
泛型类的类型参数A
可以通过使用注解+A
变成协变的。对于某个class List[+A]
,让A
协变意味着对于类型A
和B
,如果A
是B
的子类型,那么List[A]
则是List[B]
的子类型。这让我们可以使用泛型类来创建非常有用且直观的子类型关系。
现在来看下面这个简单的类结构:
1 | abstract class Animal { |
Cat
和Dog
都是Animal
的子类型。Scala的标准库中有一个常用的不可变类sealed abstract class List[+A]
,它的类型参数A
就是协变的。这意味着List[Cat]
是一个List[Animal]
而 List[Dog]
也是一个List[Animal]
。从直观上,一个猫的列表和一个狗的列表都是一个动物列表是讲得通的。因而你可以把它们其中任意一个替换为List[Animal]
来使用。
下面的例子当中,方法printAnimalNames
会接受一个动物列表作为参数,并且依次在新的一行里打印它们的名字。如果List[A]
不是协变的,则下面两个方法的调用不会通过编译,这将严重限制了printAnimalNames
方法的使用。
1 | object CovarianceTest extends App { |
逆变
泛型类的类型参数A
可以通过使用注解-A
变成逆变的。这使得我们在类和相似的类型参数之间建立起了子类型关系,而结果正好与协变相反。也就是说,对于某个类class Writer[-A]
,让A
逆变意味着对于类型A
和B
,如果A
是B
的子类型,那么Writer[B]
则是Writer[A]
的子类型。
想想上面定义的类Cat
、Dog
和Animal
,在下面例子中的应用:
1 | abstract class Printer[-A] { |
一个Printer[A]
是一个简单的类,它知道该如何打印出类型A
。下面我们为具体的类型来定义一些子类吧:
1 | class AnimalPrinter extends Printer[Animal] { |
如果一个Printer[Cat]
知道如何打印Cat
到控制台,而一个Printer[Animal]
知道如何打印Animal
到控制台,那么一个Printer[Animal]
从道理上讲也应该知道如何打印Cat
。反过来则不适用,因为一个Printer[Cat]
不知道如何打印一个Animal
到控制台。因此我们应该能够用Printer[Animal]
来替换Printer[Cat]
,要做到这点的话,需要让Printer[A]
成为逆变的。
1 | object ContravarianceTest extends App { |
该程序的输出如下:
1 | The cat's name is: Boots |
不变
Scala中的泛型类默认是不变的。这意味着它们既不是协变的,也不是逆变的。下面的例子中,Container
类是不变的,则Container[Cat]
不是Container[Animal]
,反过来也不是。
1 | class Container[A](value: A) { |
看起来似乎一个Container[Cat]
应该也是一个Container[Animal]
,但是允许一个可变的泛型类协变其实是不安全的。这个例子中,Container
是不变的,这点很重要。如果Container
是协变的,类似于下面的事情可能就会发生:
1 | val catContainer: Container[Cat] = new Container(Cat("Felix")) |
幸运的是,在我们犯错前编译器就会阻止我们。
其他示例
另外有一个可以帮助我们理解型变的例子,是源于Scala标准库里的trait Function1[-T, +R]
。Function1
是带有一个参数的函数,第一个类型参数T
表示参数类型,第二个类型参数R
表示返回类型。Function1
在它的参数类型上是逆变的,在返回类型上是协变的。一般我们会使用字面量A => B
来表示Function1[A, B]
。
假如我们已经有了和之前类似的Cat
、Dog
、Animal
继承树,外加下面这些:
1 | abstract class SmallAnimal extends Animal |
假设现在我们有个函数接受动物类型,返回它们吃的食物类型。我们想要的可能是Cat => SmallAnimal
(因为猫会吃小动物),但是如果替换为Animal => Mouse
,我们的程序也可以正常运行。直观上说Animal => Mouse
也可以接受一个Cat
作为参数,因为Cat
是一个Animal
,并且返回一个Mouse
,而它也是一个SmallAnimal
。我们可以安全且隐形地用后者进行替换,因而我们可以说Animal => Mouse
是Cat => SmallAnimal
的子类型。
和其他语言比较
和Scala类似的一些语言对于支持型变的方式是不一样的。例如,Scala中的型变注解其实和C#非常类似,它们都是在抽象类的定义时添加了注解(称为“声明式型变”)。而在Java中,型变注解是在一个抽象类使用时由使用者给定的(称为“使用式型变”)。