18.scala的型变

Posted 大数据群英萃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了18.scala的型变相关的知识,希望对你有一定的参考价值。

型变是复杂类型的子类型关系与其组件类型的子类型关系的相关性。Scala支持 泛型类 的类型参数的型变注释,允许它们是协变的,逆变的,或在没有使用注释的情况下是不变的。在类型系统中使用型变允许我们在复杂类型之间建立直观的连接,而缺乏型变则会限制类抽象的重用性。

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


协变

使用注释 +A,可以使一个泛型类的类型参数 A 成为协变。对于某些类 class List[+A],使 A 成为协变意味着对于两种类型 AB,如果 AB 的子类型,那么 List[A] 就是 List[B] 的子类型。这允许我们使用泛型来创建非常有用和直观的子类型关系。

考虑以下简单的类结构:

 
abstract class Animal { def name: String}case class Cat(name: String) extends Animalcase 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) printAnimalNames(dogs)}


逆变

通过使用注释 -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,并且 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: BootsThe animal's name is: Boots


不变


默认情况下,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],但允许一个可变的泛型类成为协变并不安全。在这个例子中,Container 是不变的非常重要。假设 Container 实际上是协变的,下面的情况可能会发生:

val catContainer: Container[Cat] = new Container(Cat("Felix"))val animalContainer: Container[Animal] = catContaineranimalContainer.setValue(Dog("Spot"))val cat: Cat = catContainer.getValue
幸运的是,编译器在此之前就会阻止我们。


其他例子

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

假设前面使用过的类似 CatDogAnimal 的继承关系,加上以下内容:

 
abstract class SmallAnimal extends Animalcase class Mouse(name: String) extends SmallAnimal

假设我们正在处理接受动物类型的函数,并返回他们的食物类型。如果我们想要一个 Cat => SmallAnimal(因为猫吃小动物),但是给它一个 Animal => Mouse,我们的程序仍然可以工作。直观地看,一个 Animal => Mouse 的函数仍然会接受一个 Cat 作为参数,因为 Cat 即是一个 Animal,并且这个函数返回一个 Mouse,也是一个 SmallAnimal。既然我们可以安全地,隐式地用后者代替前者,我们可以说 Animal => MouseCat => SmallAnimal 的子类型。

与其他语言的比较

某些与 Scala 类似的语言以不同的方式支持型变。例如,Scala 中的型变注释与 C# 中的非常相似,在定义类抽象时添加型变注释(声明点型变)。但是在Java中,当类抽象被使用时(使用点型变),才会给出型变注释。

推荐阅读:




以上是关于18.scala的型变的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin泛型的型变之路

Kotlin 型变 + 星号投影(扯蛋)

Scala型变

js如何把英文型时间转成中文的型的

尝鲜 Dart 2.7 最新语法之泛型强化:声明处型变

尝鲜 Dart 2.7 最新语法之泛型强化:声明处型变