Contravariant and Covariant

将一个实例赋予它的父类变量时,其赋值语言缺省具有 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 = ???
}
Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP