【Scala之旅】类型参数

MICKEYSTUDY 2019-06-27

本节翻译自

综述:在Scala中,你可以用类型参数来实现类和函数,这样的类和函数可以用于多种类型;你可以指定类型如何根据其类型参数的变化而变化。

泛型类

泛型类将类的类型作为参数。他们作为集合类的时候尤其有用。

定义一个泛型类

泛型类将类型作为参数放进中括号 [] 中。有一个惯例是将大写字母 A 作为类型参数的标志符,尽管任何参数类型名字都可以被使用。

class Stack[A] {
  private var elements: List[A] = Nil
  def push(x: A) { elements = x :: elements }
  def peek: A = elements.head
  def pop(): A = {
    val currentTop = peek
    elements = elements.tail
    currentTop
  }
}

这个 Stack 的实现类可以将任何类型 A 作为参数。这意味着基础列表 var elements: List[A] = Nil 只能存储类型为 A 的元素。而方法 def push 只接受类型为 A 的对象(注意:elements = x :: elements 通过将 x 预先添加到当前的 elements 创建新的列表,再重新分配给 elements)。

使用

通过将类型放入中括号中代替 A 来使用泛型类:

val stack = new Stack[Int]
stack.push(1)
stack.push(2)
println(stack.pop)  // prints 2
println(stack.pop)  // prints 1

这个实例 Stack 只能取整数。然而,如果类型参数有子类型,这些可以通过:

class Fruit
class Apple extends Fruit
class Banana extends Fruit

val stack = new Stack[Fruit]
val apple = new Apple
val banana = new Banana

stack.push(apple)
stack.push(banana)

AppleBanana 都扩展自 Fruit,所以我们可以把 AppleBanana 压入 Fruit 的堆栈中。

注意:泛型类型的子类型是不变的。这意味着如果我们有一堆类型为 Stack[Char] 的字符堆栈,那么它不能用作 Stack[Int] 类型的整数堆栈。这是有缺陷的,因为我们能够将数字真正输入到字符堆栈。总之,当且仅当 B = A 时,Stack[A] 才是 Stack[B] 的一个子类型。由于这可能是相当严格的,Scala提供了一个类型参数注解机制来控制泛型类型的子类型行为。

型变

型变是复杂类型的子类型关系以及它们的组件类型的子类型关系之间的相关性。Scala支持在泛型类的类型参数中使用型变注解;如果没有使用注解,则允许它们是协变的、逆变的或不变的。在类型系统中使用型变允许我们在复杂类型之间建立直观的联系,而缺乏形变可以限制类抽象的重用。

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

协变

一个泛型类的类型参数 A 可以通过使用注解 +A 进行协变。对一些 class List[A],将 A 协变意味着若有 AB 两种类型且 AB 的子类,则 List[A]List[B] 的一个子类。这使我们能够使用泛型做出非常有用和直观的子类型关系。

个人注:这里的意思是,原先虽然 AB 的子类,但 List[A]List[B] 并没有任何关系。而将原来的泛型类加上型变注解(class List[+A])之后,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]。直观地说,Cat 的列表和 Dog 的列表都是一个 Animal 列表,你应该能够用它代替一个 List[Animal]

在下面的例子中,方法 printAnimalNames 将接收一个 Animal 列表作为参数,然后在每个新行中打印它们自己的名字。如果 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
}

逆变

一个泛型类的类型参数 A 可以通过使用注解 -A 进行逆变。这与使用类和它的类型参数创建子类型关系类似,但与我们用协变得到的类型相反。也就是说,对一些 class Writer[A]A 逆变意味着:若有两种类型 ABAB 的子类,则 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: Boots
The 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] = catContainer
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue // Oops, we'd end up with a Dog assigned to a Cat

幸运的是,编译器在很久之前就阻止了我们。

其他例子

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

假设类似 CatDogAnimal 继承树使用前,加上以下几点:

class SmallAnimal
class Mouse extends SmallAnimal

假设我们的函数接受 Animal 类型,并返回它们吃的食物种类。如果我们像 Cat => SmallAnimal(因为猫吃小动物),但用 Animal => Mouse 来代替,我们的程序依然可以继续工作。直观来讲,Animal => Mouse 仍将接受 Cat 作为参数,因为 CatAnimal,并且返回 Mouse,它也是 SmallAnimal。既然我们可以安全地、无形地将前者替换成后者,我们可以说 Animal => MouseCat => SmallAnimal 的子类型。

与其他语言相比

一些类似于Scala的语言以不同的方式支持型变。在Scala中型变的注解相似于c#,它在定义类抽象时添加注解(声明位置变量)。但是,在Java中,当使用类抽象时(客户端使用型变),客户端会给出型变注解。

类型上界

在 Scala 中,类型参数抽象类型可能受到类型边界的约束。这种类型边界限制了类型变量的具体值,并且可能揭示了关于这些类型成员的更多信息。类型上边界 T <: A 声明类型变量 T 是类型 A 的子类型。下面是一个示例,它演示了类 PetContainer 的类型参数的类型上界绑定:

abstract class Animal {
 def name: String
}

abstract class Pet extends Animal {}

class Cat extends Pet {
  override def name: String = "Cat"
}

class Dog extends Pet {
  override def name: String = "Dog"
}

class Lion extends Animal {
  override def name: String = "Lion"
}

class PetContainer[P <: Pet](p: P) {
  def pet: P = p
}

val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)
//  val lionContainer = new PetContainer[Lion](new Lion)
//                         ^this would not compile

class PetContainer 取一个类型参数 P,它必须是 Pet 的子类型。DogCat 都是 Pet 的子类,所以我们能创建一个新的 PetContainer[Dog] 和一个新的 PetContainer[Cat]。然而。如果我们尝试去创建一个 PetContainer[Lion],我们将得到一下错误:

type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet]

这是因为 Lion 不是 Pet 的子类。

类型下界

类型上界限制了一个类型为另一种类型的子类型,而类型下界则声明一个类型为另一种类型的超类型。B >: A 语法表达了类型参数 B 或者抽象类型 B 是类型 A 的超类型。在大多数情况下,A 将是类的类型参数,B 将是方法的类型参数。

这里是一个非常有用的例子:

trait Node[+B] {
  def prepend(elem: B): Unit
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
  def prepend(elem: B) = ListNode[B](elem, this)
  def head: B = h
  def tail = t
}

case class Nil[+B]() extends Node[B] {
  def prepend(elem: B) = ListNode[B](elem, this)
}

这个程序实现了一个单链表。Nil 代表一个空元素(即一个空列表)。class ListNode 是一个节点,其中包含了 B 类型的元素(head)和对列表其余部分的引用(tail)。class Node 和他的子类是协变的,因为我们有 +B

然而这个程序并不能编译,因为 prepend 的参数 elem 的类型是 B,我们将其声明为协变。这是行不通的,因为一个函数在参数类型中应该是逆变的,在结果类型中是协变的。

为了解决这个问题,我们需要翻转 prepend 参数类型 elem 的协变。我们通过引入一个新的类型参数 U 来实现这一点,它有 B 作为一个类型下界限制。

trait Node[+B] {
  def prepend[U >: B](elem: U)
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
  def prepend[U >: B](elem: U) = ListNode[U](elem, this)
  def head: B = h
  def tail = t
}

case class Nil[+B]() extends Node[B] {
  def prepend[U >: B](elem: U) = ListNode[U](elem, this)
}

现在我们可以做到以下几点:

trait Bird
case class AfricanSwallow() extends Bird
case class EuropeanSwallow() extends Bird

val africanSwallowList= ListNode[AfricanSwallow](AfricanSwallow(), Nil())
val birdList: Node[Bird] = africanSwallowList
birdList.prepend(new EuropeanSwallow)

Node[Bird] 可以被分配到 africanSwallowList 上,但随后接受 EuropeanSwallow

多态方法

Scala中的方法可以通过类型和值进行参数化。语法类似于泛型类。类型参数用方括号括起来,而值参数用圆括号括起来。

下面是一个例子:

def listOfDuplicates[A](x: A, length: Int): List[A] = {
    if (length < 1)
        Nil
    else
        x :: listOfDuplicates(x, length - 1)
}
println(listOfDuplicates[Int](3, 4))  // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8))  // List(La, La, La, La, La, La, La, La)

listOfDuplicayes 方法获取一个类型参数 A 还有值参数 xlength。值 x 的类型是 A。如果 length < 1 我们返回一个空列表。否则,我们将 x 加入到递归调用 listOfDuplicates 返回的重复列表中。(注意 :: 表示左侧的元素为右侧的序列)。

在第一个示例调用中,我们通过写入 [Int] 来显式提供类型参数。因此,第一个参数必须是一个 Int,返回类型将是 List[Int]

第二个示例调用显示你并不总是需要显式提供类型参数。 编译器通常可以根据上下文或值参数的类型来推断它。 在这个例子中,"La"是一个 String,因此编译器知道 A 必须是 String

相关推荐

AndroidAmelia / 0评论 2020-05-27

adonislu / 0评论 2020-01-17