Post

Scala基础教程 第5节 泛型

Scala基础教程 第5节 泛型

5.1 泛型类

https://docs.scala-lang.org/tour/generic-classes.html

泛型类(generic class)是接受类型参数的类。它们对于集合类特别有用。

5.1.1 定义泛型类

泛型类在类名后的[]内声明类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Stack[A] {
  private var elements: List[A] = Nil

  def push(x: A): Unit = {
    elements = x :: elements
  }

  def peek: A = elements.head

  def pop(): A = {
    val currentTop = peek
    elements = elements.tail
    currentTop
  }
}

这段代码实现了一个接受类型参数A的泛型类Stack。这意味着底层列表elements只能存储A类型的元素。push()方法只接受A类型的对象。

注:

  • x :: elements创建了一个新列表,将x附加到当前的elements之前。
  • Nil表示空列表,不要与null混淆。

5.1.2 使用泛型类

要使用泛型类,将类型放在[]中以替换A

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

如果类型参数有子类型,也可以向push()方法传递子类型的对象:

1
2
3
4
5
6
7
8
9
10
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栈中。

注意:泛型类的继承是不变的(invariant),即Stack[A]Stack[B]的子类型当且仅当A = B。这意味着不能将Stack[Apple]类型的对象赋给Stack[Fruit]类型的变量。但是Scala提供了一种类型参数注释机制来控制泛型类型的继承行为,详见5.3节。

5.2 泛型方法

https://docs.scala-lang.org/tour/polymorphic-methods.html

在Scala中,方法也可以接受类型参数。语法与泛型类相似,类型参数用方括号括起来,放在方法名和参数列表之间。

下面是一个示例:

1
2
3
4
5
6
7
8
9
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)

方法listOfDuplicates()接受类型参数A以及值参数xlengthx的类型为A。该方法返回长度为length、元素全部是x的列表。

在第一个调用中,我们显式提供了类型参数Int。因此第一个参数必须是Int,返回类型是List[Int]

第二个调用省略了类型参数,编译器通常可以根据上下文或值参数的类型来推断它。在这个例子中,"La"是一个String,因此编译器知道A = String

5.3 协变和逆变

https://docs.scala-lang.org/tour/variances.html

变型(variance)让你能够控制类型参数在继承关系中的行为。Scala支持对泛型类的类型参数进行标注,使其可以是协变(covariant)、逆变(contravariant)或不变(invariant)(如果没有任何标注)。在类型系统中使用变型使我们能够在复杂类型之间建立直观的联系。

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

5.3.1 不变

默认情况下,Scala的类型参数是不变的(invariant):类型参数之间的继承关系不会反映在泛型类中。考虑下面的泛型类Box

1
class Box[A](var content: A)

我们将在其中存放Animal类型的对象,其定义如下:

1
2
3
4
5
6
abstract class Animal {
  def name: String
}

case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

CatDog都是Animal的子类型,这意味着以下赋值是合法的:

1
val myAnimal: Animal = Cat("Felix")

但是,Box[Cat]并不是Box[Animal]的子类型。如果试图将一个Box[Cat]对象赋给Box[Animal]变量,编译器将会报错:

1
2
3
val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
val myAnimal: Animal = myAnimalBox.content

假设这样做是合法的,由于Boxcontent字段是可变的,就可以将myAnimalBox的内容替换为一个Dog

1
myAnimalBox.content = Dog("Fido") // OK, Dog is an Animal

但是,变量myAnimalBox实际引用了一个Box[Cat]对象。此时如果获取myCatBox的内容,期望是一个Cat,但实际是一个Dog,这就破坏了类型可靠性!

1
val myCat: Cat = myCatBox.content // myCat would be a Dog!

由此可以得出结论:即使CatAnimal的子类型,Box[Cat]Box[Animal]也不能有子类型关系。

注:Java的泛型类都是不变的,详见《Java核心技术》笔记 卷I 第8章 8.7节。

5.3.2 协变

上面遇到的问题是:因为可以把Dog放入Box[Animal],所以Box[Cat]不能是Box[Animal]的子类型。

但是,如果不能把Dog放入Box[Animal](即Box是不可变的),那么就可以保证从Box[Cat]获取到的一定是Cat。这样就可以让Box[Cat]Box[Animal]的子类型:

1
2
3
4
class ImmutableBox[+A](val content: A)

val myCatBox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
val myAnimalBox: ImmutableBox[Animal] = myCatBox // now this compiles

我们说ImmutableBoxA协变的(covariant),由A前面的+表示。

一般地,给定class Cov[+T],如果AB的子类型,则Cov[A]Cov[B]的子类型。

在下面的例子中,printAnimalNames()方法接受一个List[Animal]参数,并打印它们的名字。如果List[A]不是协变的,最后两个方法调用将编译失败,这将严重限制printAnimalNames()方法的有用性。

1
2
3
4
5
6
7
8
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)  // prints: Whiskers, Tom
printAnimalNames(dogs)  // prints: Fido, Rex

5.3.3 逆变

在上一节已经看到,如果确保只读、不写,就可以定义协变类型。反过来,如果只写、不读呢?假设有一个序列化器,接受A类型的值并将其转换为序列化格式,就会出现这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Serializer[-A] {
  def serialize(a: A): String
}

class AnimalSerializer extends Serializer[Animal] {
  override def serialize(animal: Animal): String = s"""{"name": "${animal.name}"}"""
}

val animalSerializer: Serializer[Animal] = new AnimalSerializer

val catSerializer: Serializer[Cat] = animalSerializer
println(catSerializer.serialize(Cat("Felix"))) // prints: {"name": "Felix"}

我们说SerializerA逆变的(contravariant),由A前面的-表示。Serializer[Animal]Serializer[Cat]的子类型。

一般地,给定class Contra[-T],如果AB的子类型,则Contra[B]Contra[A]的子类型。

5.3.4 不可变性与变型

不可变性(immutability)是变型背后的设计决策的重要组成部分。例如,Scala的集合系统地区分了可变和不可变集合。协变的可变集合会破坏类型安全,因此集合scala.collection.immutable.List是协变的,而scala.collection.mutable.ListBuffer是不变的。

假设ListBuffer是协变的,以下有问题的代码将能够编译通过:

1
2
3
4
5
6
import scala.collection.mutable.ListBuffer

val bufInt: ListBuffer[Int] = ListBuffer(1, 2, 3)
val bufAny: ListBuffer[Any] = bufInt
bufAny(0) = "Hello"
val firstElem: Int = bufInt(0)

由于bufInt(0)包含一个String而不是Int,将其赋给firstElem将抛出ClassCastException

小结

变型语法含义适用场景
不变class C[T]如果AB的子类型,C[A]C[B]无关可变类
协变class C[+T]如果AB的子类型,则C[A]C[B]的子类型不可变类,只读不写
逆变class C[-T]如果AB的子类型,则C[B]C[A]的子类型不可变类,只写不读
  • 协变(+T)类型参数只能出现在输出位置(不可变字段、返回类型),不能出现在输入位置(方法参数)。
  • 逆变(-T)类型参数只能出现在输入位置,不能出现在输出位置。

例如,Scala标准库特质Function1简化的定义如下:

1
2
3
trait Function1[-A, +R] {
  def apply(arg: A): R
}

5.4 类型边界

在Scala中,类型参数和抽象类型成员可以受类型边界(type bound)的约束。

注:Java也有类似的类型边界概念,详见《Java核心技术》笔记 卷I 第8章 8.4节。

5.4.1 类型上界

https://docs.scala-lang.org/tour/upper-type-bounds.html

类型上界(upper type bound)用<:表示。B <: A声明B必须是A的子类型。

例如:

1
2
3
4
5
6
7
8
9
10
11
abstract class Animal
abstract class Pet extends Animal
class Cat extends Pet
class Dog extends Pet
class Lion extends Animal

class PetContainer[P <: Pet](val 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

PetContainer接受一个类型参数P,该参数必须是Pet的子类型。DogCatPet的子类型,因此可以创建PetContainer[Dog]PetContainer[Cat]。但是,Lion不是Pet的子类型,如果试图创建PetContainer[Lion]会得到以下错误:

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

5.4.2 类型下界

https://docs.scala-lang.org/tour/lower-type-bounds.html

类型下界(lower type bound)用>:表示。B >: A声明B必须是A的超类型。在大多数情况下,A是类的类型参数,B是方法的类型参数。

下面是一个示例:

1
2
3
4
5
6
7
trait List[+A] {
  def prepend(elem: A): NonEmptyList[A] = NonEmptyList(elem, this)
}

case class NonEmptyList[+A](head: A, tail: List[A]) extends List[A]

case object Nil extends List[Nothing]

这个程序实现了一个单向链表。Nil表示空链表。类NonEmptyList表示一个节点,包含一个A类型的元素(head)和列表其余部分的引用(tail)。特质List及其子类型是协变的,因为声明了+A

然而,这个程序无法编译,因为prepend()方法的参数elem是协变类型A方法的参数类型是逆变的,返回类型是协变的。

为了解决这个问题,需要翻转elem类型的变型。可以通过引入一个以A为类型下界的新的类型参数B来实现:

1
2
3
trait List[+A] {
  def prepend[B >: A](elem: B): NonEmptyList[B] = NonEmptyList(elem, this)
}

现在可以这样使用List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Animal
class Cat extends Animal
class Dog extends Animal

val dogs: List[Dog] = Nil.prepend(new Dog)
val catsFromAntarctica: List[Animal] = Nil
val someAnimal: Animal = new Cat

// OK, List is covariant in A
val animals: List[Animal] = dogs

// OK, A = Dog, B = Animal
val someAnimals = dogs.prepend(someAnimal) // NonEmptyList[Animal]

// OK, A = Animal, B = Animal
val moreAnimals = animals.prepend(new Cat) // NonEmptyList[Animal]

// OK, A = Dog, B = Animal (because that is the supertype common to Cat and Dog)
val allAnimals = dogs.prepend(new Cat) // NonEmptyList[Animal]

// mistake: A = Animal, B = Object (adding a list widens the type arg too much, -Xlint will warn)
val error = moreAnimals.prepend(catsFromAntarctica) // NonEmptyList[Object]

注意:

  • dogs.prepend(new Cat)List[Dog]添加了一个Cat,看起来是有问题的。实际上,编译器会将prepend()方法的类型参数B推导为Animal(即CatDog的公共超类),因此返回类型为NonEmptyList[Animal]而不是NonEmptyList[Dog]
  • 由于List[A]是协变的,一个List[Animal]变量实际可能引用List[Animal]List[Cat]List[Dog]对象。换句话说,类型参数A代表了以A为上界的类层次结构(即“A或其子类型”)。在类型边界中,A只能作为下界(因为AnimalCatDog没有公共子类型,但有公共超类型)。同理,逆变类型参数只能作为类型上界。

小结

类型边界语法含义
类型上界B <: AB必须是A的子类型
类型下界B >: AB必须是A的超类型

变型与类型边界的关系:

变型上界B <: A下界B >: A
不变A
协变+A×
逆变-A×
This post is licensed under CC BY 4.0 by the author.