类型参数方差控制参数化类型(如类或特质)的子类型化。
为了解释方差,我们假设以下类型定义
trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }
我们还假设以下参数化类型
// an example of an invariant type
trait Pipeline[T] {
def process(t: T): T
}
// an example of a covariant type
trait Producer[+T] {
def make: T
}
// an example of a contravariant type
trait Consumer[-T] {
def take(t: T): Unit
}
// an example of an invariant type
trait Pipeline[T]:
def process(t: T): T
// an example of a covariant type
trait Producer[+T]:
def make: T
// an example of a contravariant type
trait Consumer[-T]:
def take(t: T): Unit
通常有三种方差模式
- 不变—默认,类似于
Pipeline[T] - 协变—使用
+注释,例如Producer[+T] - 逆变—使用
-注释,例如Consumer[-T]
我们现在将详细说明此注释的含义以及我们使用它的原因。
不变类型
默认情况下,Pipeline 等类型在其类型参数(本例中为 T)中是不变的。这意味着 Pipeline[Item]、Pipeline[Buyable] 和 Pipeline[Book] 等类型彼此没有子类型关系。
这是正确的!假设有以下方法,它使用两个 Pipeline[Buyable] 类型的变量,并根据价格将参数 b 传递给其中一个变量
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable = {
val b1 = p1.process(b)
val b2 = p2.process(b)
if (b1.price < b2.price) b1 else b2
}
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable =
val b1 = p1.process(b)
val b2 = p2.process(b)
if b1.price < b2.price then b1 else b2
现在,请回想我们类型之间的子类型关系如下
Book <: Buyable <: Item
我们无法将 Pipeline[Book] 传递给 oneOf 方法,因为在其实现中,我们使用 Buyable 类型的变量调用 p1 和 p2。而 Pipeline[Book] 期望 Book,这可能会导致运行时错误。
我们无法传递 Pipeline[Item],因为在它上面调用 process 只承诺返回 Item;但是,我们应该返回 Buyable。
为什么是不变的?
事实上,Pipeline 类型需要是不变的,因为它同时将类型参数 T 用作参数和返回类型。出于同样的原因,Scala 集合库中的一些类型(如 Array 或 Set)也是不变的。
协变类型
与不变的 Pipeline 相反,Producer 类型通过在类型参数前加上 + 标记为协变。这是有效的,因为类型参数仅在返回位置中使用。
将其标记为协变意味着我们可以传递(或返回)Producer[Book],而 Producer[Buyable] 是预期的。事实上,这是合理的。Producer[Buyable].make 的类型只承诺返回Buyable。作为 make 的调用方,我们很乐意接受 Book,它是 Buyable 的子类型,即它至少是 Buyable。
以下示例对此进行了说明,其中函数 makeTwo 期待 Producer[Buyable]
def makeTwo(p: Producer[Buyable]): Int =
p.make.price + p.make.price
传递书籍的生产者完全没问题
val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)
在 makeTwo 中对 price 的调用对于书籍仍然有效。
不可变容器的协变类型
在处理不可变容器时,您会经常遇到协变类型,例如可以在标准库中找到的那些(例如 List、Seq、Vector 等)。
例如,List 和 Vector 大致定义为
class List[+A] ...
class Vector[+A] ...
这样,您可以在预期 List[Buyable] 的地方使用 List[Book]。这在直觉上也说得通:如果您预期的是一个可以购买的事物集合,那么给您一个书籍集合应该是可以的。在我们的示例中,它们有一个额外的 ISBN 方法,但您可以随意忽略这些附加功能。
逆变类型
与标记为协变的类型 Producer 相反,类型 Consumer 通过在类型参数前加上 - 标记为逆变。这是有效的,因为类型参数仅用于参数位置。
将其标记为逆变意味着我们可以传递(或返回)Consumer[Item],而预期的是 Consumer[Buyable]。也就是说,我们有子类型关系 Consumer[Item] <: Consumer[Buyable]。请记住,对于类型 Producer,情况正好相反,我们有 Producer[Buyable] <: Producer[Item]。
事实上,这是合理的。方法 Consumer[Item].take 接受 Item。作为 take 的调用方,我们还可以提供 Buyable,Consumer[Item] 会乐于接受它,因为 Buyable 是 Item 的子类型——也就是说,它至少是 Item。
消费者协变类型
协变类型比协变类型少得多。如我们在示例中看到的,你可以将它们视为“消费者”。你可能遇到的最重要的标记为协变的类型是函数类型
trait Function[-A, +B] {
def apply(a: A): B
}
trait Function[-A, +B]:
def apply(a: A): B
它的参数类型 A 被标记为协变 A——它消耗类型 A 的值。相反,它的结果类型 B 被标记为协变——它生成类型 B 的值。
以下是一些示例,说明了方差注释在函数上引起的子类型关系
val f: Function[Buyable, Buyable] = b => b
// OK to return a Buyable where a Item is expected
val g: Function[Buyable, Item] = f
// OK to provide a Book where a Buyable is expected
val h: Function[Book, Buyable] = f
摘要
在本节中,我们遇到了三种不同的方差
- 生产者通常是协变的,并用
+标记其类型参数。这同样适用于不可变集合。 - 消费者通常是协变的,并用
-标记其类型参数。 - 既是生产者又是消费者的类型必须是不变的,并且不需要在其类型参数上进行任何标记。像
Array这样的可变集合属于此类别。