将一个实例赋予它的父类变量时,其赋值语言缺省具有 Covariant(协变)
语意:
trait Animal
case class Dog(name:String) extends Animal
val animal:Animal = Dog("Max")
协变的语意是:当一个类型A(Animal)要接纳一个子类型B(Dog)的赋值时,尝试将B变为A的子类,系标记为+A
。继承的父<-子方向为 A <- +A
。
协变的语意和子类型多态的变态方向是相同的。正如上所示,Animal 是 Dog 的父类,因此将 Dog cast 成 Animal 是可以接受的。
容器类不具有类型继承关系,所以以下无法通过编译检查:
class MyList[T]
val myAnimals:MyList[Animal] = new MyList[Dog] // 非法
因为 MyList[Dog] 不是 MyList[Animal] 的子类,但是可以通过协变
来声明他们参数之间的的关系从而声明成父子:
class MyList[+T] // 声明允许Animal将Dog(+A)视为子类,从而允许将MyList[Dog]转为MyList[Animal]
val myAnimals:MyList[Animal] = new MyList[Dog] // 合法
而 Contravariant(逆变)
则将父子的关系声明为反方向:
逆变的语意是:当一个类型A(Animal)向子类型B(Dog)赋值时,尝试将B变成A的父类,标记为-A。继承的父<-子方向为 -A <- A
class MyList[-T] // 声明允许将Dog(-A)视为Animal的父类,从而允许将MyList[Animal]转为MyList[Dog]
val myDogs:MyList[Dog] = new MyList[Animal] // 合法,虽然会失败
逆变
声明通常是危险的,因为赋值语句中的类型变态的方向是从右向左
。而如果赋值语句的左边(被赋值端)的类型是确定的,并且是赋值端(右侧)的子类,那不能满足类型转换条件。因为 assignment 通常允许我们做减法
,可以想象将一个子类,比如 Dog 赋予它的父类 Animal 时,Dog 携带的信息等于或多余 Animal 所需要的信息,也就是说父类通常是子类的子集,因此必然可以通过子类构建出父类,所以(协变)是安全的。而反之,将父类逆变为子类则可能因为信息不足而导致失败,因此通常情况下逆变不被缺省支持:
val dog:Dog = new Animal // 非法
但是作为函数的参数,逆变是受欢迎的:
case class Cage[-T]() { // Contravariant
def transfer(f:T) = ???
}
当以上 transfer 被调用时:
val cage = Cage[Dog]()
cage.transfer(Dog("Max")) // 合法
注意我们为方法 transfer
的参数类型 T
声明它支持逆变
,因此编译器将T
视为 Dog 的父类,标记为-Dog
,从父到子的继承关系:-Dog <- Dog
,从而允许将 Dog 逆变为 -Dog。
实际上我们并没有给 T
设定具体的边界,因此 -T 实际上指向 Dog 自己(自己是自己的父类),也可以一直逆变为 Object
为止,编译器只需要寻找一个能够接纳输入值的类型即可。如果要更明确地限定 T
的类型上限只能到 Animal
,可以指定它的上限 -T <: Animal
,并且这样可以允许接纳更多的 Animal。
val cage = Cage[Animal]() // -T == Animal
cage.transfer(Dog("Max"))
cage.transfer(Cat("MiaoMiao"))
cage.transfer(Human("小王")) // 失败,如果 Human 不是 Animal 的子类
显然这是我们希望看到的。为什么呢?因为当逆变
作用于函数的输入参数时,它迫使赋值端的类型升级为输入值的父类,从而允许接受它的所有子类为输入条件,这让函数变得更安全。
但是,如果我们将逆变
改为协变
:
case class Cage[+T]() { // Covariant
def transfer(f:T) = ???
}
编译器允许 +T
,也就是 +Dog
,做为Dog
的子类,从而形成从父到子的继承关系:Dog <- +Dog
,并尝试将 Dog 赋予自己的子类型 +Dog,但是这与赋值语句的类型转变方向(从右到左)向矛盾,因此编译器拒绝向下转换。
再来看一下函数的输出值类型的转变。与输入恰好相反,因为输出的被赋值类型(左侧)是确定的,输出类型必须确定无误地是它的子类。
case class Cage1[+O]() {
def transfer():O = ???
}
val d:Cat = Cage1[Cat]().transfer()
这满足我们对赋值的一般印象,也是前面解释的协变发生的场景。因此输出类型参数应该满足协变
。
综上所述,在定义一个容器的时候,对于参数的类型通常也应该声明为(Contravariant)逆变
类型,也就是允许被赋值端的参数逆变成输入值(赋值端)的父类,这样这个容器的实现条件可以更宽松(父类具有更少的属性),比如逆变成一个 Animal 类型的输入,那么允许我们在实现的时候接受 Dog 或 Cat 或其他任何它的子类型。而输出
类型为协变
,这样保证输出的信息足够多(子类包含的信息比父类更多),以避免无法被左侧(被赋值侧)接受:
// 输入条件放宽:接受 Animal 的任何子类型作为输入。
// 输出条件严格:输出条件限定为 Animal 的具体子类型。
//
trait Cage[-I <: Animal, +O <: Animal] {
def transfer(f:I):O
}
class DogCage extends Cage[Dog, Cat]() {
override def transfer(a: Dog): Cat = ???
}