1. 概述

本教程将深入探讨 Scala 中的变型(variance)机制。简单来说,变型描述的是类型构造器(type constructor)之间的子类型关系,它基于其绑定的具体类型之间的子类型关系来定义。

我们将介绍三种主要的变型形式:不变(invariance)协变(covariance)逆变(contravariance),并分析它们之间的区别。

2. 子类型与类型构造器

在任何编程语言中,类型(type) 都是用于告诉程序在运行时如何处理值的重要信息。子类型(subtyping) 则是在类型的基础上增加了一些约束。

对于简单的类型来说,子类型关系非常直观:

sealed trait Test 
class UnitTest extends Test 
class IntegrationTest extends UnitTest 
class FunctionalTest extends IntegrationTest

在这个例子中:

  • FunctionalTestIntegrationTest 的子类型
  • IntegrationTestUnitTest 的子类型

此外,许多编程语言也支持泛型类型(或称类型构造器)。类型构造器是一种机制,通过它我们可以基于已有的类型创建新的类型,并通过类型参数来绑定具体的类型。

比如,我们想表示对某个泛型类型 T 对象的测试结果,就可以使用类型构造器来建模:

class TestResult[T](id: String, target: T) {
  // Class behavior
}

变型机制定义了类型构造器之间的子类型关系,这种关系是基于其类型参数的子类型关系推导出来的。换句话说,对于一个类型构造器 F[_],如果 BA 的子类型,那么变型描述了 F[B]F[A] 之间的关系。

3. 变型的三种形式

变型有三种形式:协变(covariance)逆变(contravariance)不变(invariance)。下面我们逐一讲解。

3.1. 协变(Covariance)

协变是最容易理解的变型形式。我们说一个类型构造器 F[_]协变的,如果当 BA 的子类型时,F[B] 也是 F[A] 的子类型。

在 Scala 中,我们使用 +T 的语法来声明一个协变类型参数。

继续使用前面的类型层次结构,假设我们要将多个测试组合成一个测试套件(suite):

class TestsSuite[+T](tests: List[T])

我们声明了 TestsSuite 是协变的,因此 TestsSuite[IntegrationTest]TestsSuite[Test] 的子类型。这意味着我们可以这样写:

val suite: TestsSuite[Test] = new TestsSuite[UnitTest](List(new UnitTest))

✅ 每当我们需要一个 TestsSuite[T] 类型的变量时,都可以用 TestsSuite[R] 类型的对象来赋值,只要 RT 的子类型即可。

协变是类型安全的,因为它符合我们对子类型的基本理解:子类型的对象可以安全地赋值给父类型的变量。

如果我们去掉协变标记 +,编译器会报错:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.UnitTest]
 required: com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.Test]
Note: com.baeldung.scala.variance.Variance.UnitTest <: com.baeldung.scala.variance.Variance.Test, but class TestsSuits is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

⚠️ Scala 标准库中有许多协变类型构造器,例如 List[T]Option[T]Try[T] 等。

3.2. 逆变(Contravariance)

逆变是协变的反面。我们说一个类型构造器 F[_]逆变的,如果当 BA 的子类型时,F[A]F[B] 的子类型。

在 Scala 中,我们使用 -T 的语法来声明一个逆变类型参数。

乍一看,逆变似乎有点反直觉。我们为什么需要它呢?来看一个例子:

class Person(val name: String)
class Employee(name: String, val salary: Int) extends Person(name)
class Manager(name: String, salary: Int, val manages: List[Employee]) extends Employee(name, salary)

我们定义一个测试断言类:

class Assert[-T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

这个类接受一个函数 expr,用于判断类型为 T 的对象是否满足某个条件。

我们可以创建多个断言:

val personAssert = new Assert[Person](p => p.name == "Alice")
val employeeAssert = new Assert[Employee](e => e.name == "Bob" && e.salary > 0) 
val managerAssert = new Assert[Manager](m => m.manages.nonEmpty)

现在我们想组合多个断言执行:

trait Asserts[T] {
  def asserts: List[Assert[T]]
  def execute(target: T): Boolean =
    asserts
      .map(a => a.assert(target))
      .reduce(_ && _)
}

定义一个针对 Employee 的断言组合:

class AssertsEmployee(val asserts: List[Assert[Employee]]) extends Asserts[Employee]

由于 Assert 是逆变的,Assert[Person]Assert[Employee] 的子类型,因此我们可以把 personAssertemployeeAssert 放在一起:

val bob = new Employee("Bob", 50000) 
val tester = new AssertsEmployee(List(personAssert, employeeAssert)) 
tester.execute(bob)

✅ 这样我们就可以用 Assert[Person] 来测试 Employee 对象,因为 Employee 包含了 Person 的所有属性。

如果我们去掉逆变标记 -,编译器会报错:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
Note: com.baeldung.scala.variance.Variance.Person >: com.baeldung.scala.variance.Variance.Employee, but class Assert is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)

❌ 如果我们试图把 Assert[Manager] 放进 Assert[Employee] 的列表中,编译器也会报错,因为 ManagerEmployee 的子类型,而逆变要求的是父类型可以替代子类型

在 Scala 标准库中,最典型的逆变类型构造器是 Function1[-T1, +R],表示一个接受类型为 T1 参数、返回类型为 R 的函数。

3.3. 不变(Invariance)

如果一个类型构造器既不是协变也不是逆变,那它就是不变的(invariant)。这意味着类型构造器不会保留其类型参数之间的子类型关系。

比如,如果我们去掉 Assert 类型参数的逆变标记:

class Assert[T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

然后尝试这样赋值:

val personAssert: Assert[Person] = new Assert[Employee](p => p.name == "Alice")

编译器会报错:

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
Note: com.baeldung.scala.variance.Variance.Employee <: com.baeldung.scala.variance.Variance.Person, but class Assert is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

❌ 不变类型不允许在子类型和父类型之间进行类型替换。Java 和 C++ 中的泛型类型默认都是不变的。

4. 总结

本文介绍了类型构造器和变型的基本概念。从简单类型的子类型关系出发,我们展示了如何将这种关系扩展到泛型类型中,并引入了变型机制。

我们详细讲解了三种变型形式:

  • 协变(+T):用于类型作为输出(返回值)的场景
  • 逆变(-T):用于类型作为输入(方法参数)的场景
  • ⚠️ 不变(T):默认行为,不支持子类型替换

总的来说,当你使用泛型类型作为返回值时,应该使用协变;作为参数时,应该使用逆变

本文的完整代码示例可以在 GitHub 上找到。


原始标题:Variances in Scala

« 上一篇: Scala 枚举使用指南