Scala之旅(TOUR OF SCALA)——变化 (VARIANCES)

总的来说,协变就是类型参数是父子关系,对应的泛型类也就是父子关系。逆变与协变相反,对应泛型类是子父关系。不变就是一个类型的泛型类只能放入这种泛型类的对象,不存在父子关系。

第一句不会翻译 Scala 支持泛型类的类型参数使用变化注解,来允许泛型类协变,逆变,和默认的不变。在类型系统中使用变化允许我们在复杂类型之间建立直观地联系。缺少变化会限制抽象类的重用。

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

协变(Covariance)

泛型类的类型参数 A 可以使用 +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

Cat 类和 Dog 类都是 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 = {
        animal.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 calss 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): Uint = 
        println("The cat's name is: " + cat.name)
}

如果一个 Printer[Cat] 类知道如何向控制台输出 Cat 类,而一个 Printer[Animal] 类知道如何向控制台输出 Animal 类。那么一个 Printer[Animal] 类肯定知道如何输出 Cat 类。反过来则不行,因为一个 Printer[Cat] 不知道如何向控制台输出 Animal 类。然而,如果我们希望 Printer[-A] 的逆变可以让我们做到这一点,让一个 Printer[Animal] 类替代一个 Printer[Cat] 类。

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 setValueL(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 // 我们最终将一只狗赋值给了一只猫

幸运地是,编译器在第二行代码就阻止了我们。

其它例子(Other Examples)

另外一个能帮我们理解协变的例子是 Scala 标准库中的 trait Function1[-T, +R]Function1 代表只有一个参数的函数,第二个参数 R 代表函数的返回类型。一个 Fucntion1 根据参数是逆变的,而根据返回类型是协变的。这个例子中我们可以使用文字符号 A => B 代替 Function1[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 中变化注解与C#中定义抽象类中的添加的注解相似。在Java语言中,当一个抽象类䘣使用时客户端提供变化注解。

Guess you like

Origin blog.csdn.net/cuipp0509/article/details/80257086