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
在这个例子中:
FunctionalTest
是IntegrationTest
的子类型IntegrationTest
是UnitTest
的子类型
此外,许多编程语言也支持泛型类型(或称类型构造器)。类型构造器是一种机制,通过它我们可以基于已有的类型创建新的类型,并通过类型参数来绑定具体的类型。
比如,我们想表示对某个泛型类型 T
对象的测试结果,就可以使用类型构造器来建模:
class TestResult[T](id: String, target: T) {
// Class behavior
}
变型机制定义了类型构造器之间的子类型关系,这种关系是基于其类型参数的子类型关系推导出来的。换句话说,对于一个类型构造器 F[_]
,如果 B
是 A
的子类型,那么变型描述了 F[B]
和 F[A]
之间的关系。
3. 变型的三种形式
变型有三种形式:协变(covariance)、逆变(contravariance) 和 不变(invariance)。下面我们逐一讲解。
3.1. 协变(Covariance)
协变是最容易理解的变型形式。我们说一个类型构造器 F[_]
是协变的,如果当 B
是 A
的子类型时,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]
类型的对象来赋值,只要 R
是 T
的子类型即可。
协变是类型安全的,因为它符合我们对子类型的基本理解:子类型的对象可以安全地赋值给父类型的变量。
如果我们去掉协变标记 +
,编译器会报错:
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[_]
是逆变的,如果当 B
是 A
的子类型时,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]
的子类型,因此我们可以把 personAssert
和 employeeAssert
放在一起:
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]
的列表中,编译器也会报错,因为 Manager
是 Employee
的子类型,而逆变要求的是父类型可以替代子类型。
在 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 上找到。