类型参数方差控制参数化类型(如类或特质)的子类型化。
为了解释方差,我们假设以下类型定义
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
这样的可变集合属于此类别。