Scala之旅-变性

Scala之旅-变性

变性(Variances)

变性是复杂类型的子类型关系以及它们的组成类型的子类型关系之间的相互关系。Scala支持对泛型类的类型参数的变性注解,从而使它们变为协变的(covariant),逆变的(contravariant),或者是不变的(invariant),如果没有使用注解的话就是不变的。在类型系统中使用变性允许我们对复杂的类型之间获得符合直觉的关联关系,如果缺少了变性则会限制对抽象类的复用。

class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A]  // An invariant class

协变(Covariance)

泛型类的类型参数A可以通过使用注解+A变为协变的。对于类List[+A],使A变为协变的,意味着对于两个类型A和B,且A是B的子类型,则List[A]也是List[B]的子类型。这允许我们使用泛型得到非常有用并且符合直觉的子类型关系。

考虑如下这个简单的类结构:

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

Cat和Dog都是Animal的子类型。Scala标准库有一个泛型不能变的抽象类封装(generic immutable sealed abstract class)List[+A],其中的类型参数A是协变的。这意味着List[Cat]是一个List[Animal],并且List[Dog]也是一个List[Animal]。凭直觉,cats列表和dogs列表都是animals列表,你应该可以用它们的任何一个代替List[Animal]。

下面的例子中,方法printAnimalNames将会接受一个animals列表作为参数,并且依次在每一行中打印出它们的names。如果 List[A]不是协变的,最后两个方法调用就无法编译,这将会严重地限制方法printAnimalNames的使用。

object CovarianceTest extends App {
  def printAnimalNames(animals: List[Animal]): Unit = {
    animals.foreach { animal =>
      println(animal.name)
    }
  }

  val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
  val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))

  printAnimalNames(cats)
  // Whiskers
  // Tom
  printAnimalNames(dogs)
  // Fido
  // Rex
}

逆变(Contravariance)

泛型类的类型参数A可以通过使用注解-A变为逆变的。这在类和它的类型参数之间创建了一个跟协变类似但是完全相反的子类型关系。对于类Writer[-A],使A变为逆变的,意味着对于两个类型A和B,且A是B的子类型,则Writer[B]是Writer[A]的子类型。

考虑将上面定义的类Cat,Dog和Animal用于如下例子:

abstract class Printer[-A] {
  def print(value: A): Unit
}

Printer[A]是一个知道如何打印某个类型A的简单类。让我们针对特定类型定义一些子类:

class AnimalPrinter extends Printer[Animal] {
  def print(animal: Animal): Unit =
    println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
  def print(cat: Cat): Unit =
    println("The cat's name is: " + cat.name)
}

如果Printer[Cat]知道如何将任何Cat打印到终端,Printer[Animal]知道如何将任何Animal打印到终端,则Printer[Animal]也知道如何打印任何Cat是合理的。相反的关系则不适用,因为Printer[Cat]不知道如何将任何Animal打印到终端。因此,我们应该能够用Printer[Animal]代替Printer[Cat],使Printer[A]变为逆变的就会允许我们这样做。

object ContravarianceTest extends App {
  val myCat: Cat = Cat("Boots")
  def printMyCat(printer: Printer[Cat]): Unit = {
    printer.print(myCat)
  }

  val catPrinter: Printer[Cat] = new CatPrinter
  val animalPrinter: Printer[Animal] = new AnimalPrinter
  printMyCat(catPrinter)
  printMyCat(animalPrinter)
}

上面程序的输出如下:

The cat's name is: Boots
The animal's name is: Boots

不变(Invariance)

Scala中的泛型类默认是不变的。这意味着它们既不是协变的,也不是逆变的。在如下的例子中,类Container是不变的。一个Container[Cat]不是一个Container[Animal],反过来同样不成立。

class Container[A](value: A) {
  private var _value: A = value
  def getValue: A = _value
  def setValue(value: A): Unit = {
    _value = value
  }
}

可能看起来一个Container[Cat]很自然地也应该是一个Container[Animal],但是允许一个可变的(mutable)泛型类变为协变的是不安全的。在这个例子中,Container是不变的(invariant)这点很重要。假设Container实际上是协变的,像这样的事情就会发生:

val catContainer: Container[Cat] = new Container(Cat("Felix"))
val animalContainer: Container[Animal] = catContainer
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue // Oops, we'd end up with a Dog assigned to a Cat

幸运的是,编译器会在我们走到这之前就阻止我们。

其它例子

另外一个可以帮助理解变性的例子是来自Scala标准库的特征Function1[-T, R]。Function1表示带有一个参数的函数,它的第一个类型参数T表示参数类型,第二个类型参数R表示返回类型。Function1的参数类型是逆变的,返回类型是协变的。在这个例子中,我们将会用A => B代表Function1[A, B]。

假设之前使用的Cat,Dog和Animal继承树,再加上如下的:

class SmallAnimal
class Mouse extends SmallAnimal

假设我们的函数功能是接受animals类型,返回它们所吃的食物的类型。如果我们本希望Cat => SmallAnimal(因为cats吃small animals),但是用Animal => Mouse代替它,我们的程序仍然会正常工作。凭直觉,Animal => Mouse仍然会接受一个Cat作为一个参数,因为Cat是一个Animal,并且会返回一个Mouse,Mouse也是一个SmallAnimal。由于我们可以安全并且隐式地用Animal => Mouse代替Cat => SmallAnimal,我们就能说前者是后者的一个子类型。

与其它语言比较

与Scala类似的一些语言通过不同的方式来支持变性。举个例子,Scala中的变性注解与C#非常类似,都是在定义一个抽象类时加上注解(declaration-site variance)。然而在Java中,当使用一个抽象类时由用户指定变性注解(use-site variance)。

参考资料

本文译自Tour Of Scala – Variances

上一篇:Scala之旅-泛型类

下一篇:Scala之旅-类型上界

发表评论

电子邮件地址不会被公开。 必填项已用*标注