Scala之旅-变化(VARIANCES)

变化(Variance)是复杂类型的子类型关系组件类型的子类型关系之间的相关性。Scala 支持泛型类的参数类型的变化注解(variance annotations),如果没有使用注解,则允许它们是协变的(covariant),逆变的(contravariant)或不可变的*(invariant)。

在类型系统中使用变化(variance)允许我们在复杂类型之间做出直观的连接,而缺少变化( variance) 可能会限制抽象类的复用。

class Foo[+A] // 协变类
class Bar[-A] //  逆变类
class Baz[A]  //  不变类

协变(Covariance)

泛型类的类型参数 可以使用注解 +A 进行协变。对于某个 class List[+A],使 A 协变表示对于 AB 类型而言,如果 AB 的子类型,那么 List[A] 就是 List[B] 的子类型。这使得我们可以使用泛型来做非常有用和直观的子类型关系。
考虑这个简单的类结构

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

CatDog 都是 Animal 子类型。Scala 标准库中有一个通用不变的类 sealed abstract class List[+A],其中的类型参数 A 是协变的。这意味着 List[Cat] 是一个 List[Animal] 并且 List[Dog] 也是一个 List[Animal] 。直观的说,每一个猫列表和狗列表都是动物列表,你可以把它们中任何一个替换为 List[Animal]

在接下来的例子里,方法 printAnimalNames 将接受一个动物集合作为参数并在新行上分别输出它们的名称。如果 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 进行逆变。这在类与它的类型参数之间建立了子类型关系,但与我们获得的协变类型是相反的。也就是说,对于某个类 class Writer[-A],使 A 逆变意味着对于 AB 两种类型来说,如果 AB 的子类型,那么 Writer[B] 就是 Writer[A] 的子类型。

接下来的例子仍然使用上面定义的 CatDogAnimal

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 到控制台,并且 Print[Animal] 知道如何打印 Animal 到控制台,那就意味着 Print[Animal] 也知道如何打印 Cat。相反则不适用,因为 Printer[Cat] 不知道如何打印 Animal 到控制台。因此,如果我们愿意,我们应该可以使用 Print[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)
}

Scala之旅-变化(VARIANCES)_第1张图片
程序的输出结果为:

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

Scala之旅-变化(VARIANCES)_第2张图片

不可变(Invariance)

Scala 中的泛型类默认是不可变的。这意味着它们既不是协变(covariant) 也不是逆变(contravariant)。下面的例子中,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],但是让一个可变的泛型类成为协变类型将是不安全的。在这个例子中,Container 不可变是很重要的。假设 Container 实际上是协变的,那么有时可能会发生以下情况:

val catContainer: Container[Cat] = new Container(Cat("Felix"))
val animalContainer: Container[Animal] = catContainer
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue // 我们把狗分配给了猫

这里写图片描述
编译器提示错误:Container[Cat] 类型表达式不符合预期的 Container[Animal] 类型。
幸运的是,编译器会自动终止我们这个错误的程序。

其他例子

另一个可以帮助理解变化(variance)的例子是来源于 Scala 标准库的 trait Function1[-T, +R]Functionl 是带一个参数的函数,它的第一个参数类型 T 表示参数的类型,第二个参数类型 R 表示返回类型。Functionl 在它的参数类型上是逆变的,返回类型是协变的。对于这个例子,我们将使用文字符号 A => B 表示 Functionl[A,B]

假设在先前使用的 CatDogAnimal 继承树基础上再增加以下类:

abstract class SmallAnimal extends Animal
case class Mouse(name: String) extends SmallAnimal

假设我们这个函数接受动物类型并返回它们吃的食物类型。如果我们想要一只 Cat => SmallAnimal(因为猫爱吃小动物),但是用 Animal => Mouse 代替,我们的程序也会正常工作。直观来讲 Animal => Mouse 也可以接受一个 Cat 作为参数,因为 Cat 就是 Animal,并且返回 Mouse(也是 SmallAnimal)。既然我们可以安全隐形的使用后者替代前者,那么我们可以说 Animal => MouseCat => SmallAnimal 的子类型。

与其他语言比较

一些类似于 Scala 的语言以不同的方式支持变化(Variance)。例如,Scala 中的变化注解(variance annotations)与 C# 中的非常类似,其中在抽象类定义时就已添加注解(声明式 variance)。但是,在 Java 中,当抽象类被使用时,客户端才会提供注解(使用式 variance)。

你可能感兴趣的:(scala,Scala之旅(翻译))