Scala 3 — 书籍

方差

语言

类型参数方差控制参数化类型(如类或特质)的子类型化。

为了解释方差,我们假设以下类型定义

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 类型的变量调用 p1p2。而 Pipeline[Book] 期望 Book,这可能会导致运行时错误。

我们无法传递 Pipeline[Item],因为在它上面调用 process 只承诺返回 Item;但是,我们应该返回 Buyable

为什么是不变的?

事实上,Pipeline 类型需要是不变的,因为它同时将类型参数 T 用作参数返回类型。出于同样的原因,Scala 集合库中的一些类型(如 ArraySet)也是不变的

协变类型

与不变的 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 的调用对于书籍仍然有效。

不可变容器的协变类型

在处理不可变容器时,您会经常遇到协变类型,例如可以在标准库中找到的那些(例如 ListSeqVector 等)。

例如,ListVector 大致定义为

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 的调用方,我们还可以提供 BuyableConsumer[Item] 会乐于接受它,因为 BuyableItem 的子类型——也就是说,它至少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 这样的可变集合属于此类别。

此页面的贡献者