类型参数_型变_控制参数化类型(如类或 traits)的子类型。
为了解释型变,让我们假设以下类型定义:
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
方法,因为在其实现中,我们调用的 p1
和 p2
是 Buyable
类型的值。
Pipeline[Book]
需要的是 Book
,这可能会导致运行时错误。
我们不能传递一个 Pipeline[Item]
因为在它上面调用 process
只会保证返回一个 Item
;但是,我们应该返回一个 Buyable
。
为什么是不变的?
事实上,Pipeline
类型需要是不变的,因为它使用它的类型参数 T
既_作为参数类型,_又_作为返回类型。
出于同样的原因,Scala 集合库中的某些类型——例如 Array
或 Set
—— 也是_不变的。
协变类型
与不变的 Pipeline
相比,Producer
类型通过在类型参数前面加上 +
前缀被标记为 协变。
这是有效的,因为类型参数仅用于_返回的位置_。
将其标记为协变意味着当需要 Producer[Buyable]
时,我们可以传递(或返回)一个 Producer[Book]
。
事实上,这是合理的。 Producer[Buyable].make
的类型只承诺_返回_ Buyable
。
作为 make
的调用者,我们乐意接受作为 Buyable
的子类型的 Book
类型,—也就是说,它_至少_是一个 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[Buyable]
时,可以传递(或返回) Consumer[Item]
。
也就是说,我们有子类型关系Consumer[Item] <: Consumer[Buyable]
。
请记住,对于类型 Producer
,情况正好相反,我们有 Producer[Buyable] <: Producer[Item]
。
事实上,这是合理的。 Consumer[Item].take
方法接受一个 Item
。
作为 take
的调用者,我们还可以提供 Buyable
,它会被 Consumer[Item]
愉快地接受,因为 Buyable
是 Item
的一个子类型——也就是说,它_至少_是 Item
。
消费者的逆变类型
逆变类型比协变类型少得多。 在我们的示例中,您可以将它们视为“消费者”。你可能来的最重要的类型标记为逆变的 cross 是函数之一:
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
这样的可变集合就属于这一类。
Contributors to this page:
Contents
- 导言
- Scala 3 特性
- 为什么是 Scala 3 ?
- Scala 的味道
- Hello, World!
- The REPL
- 变量和数据类型
- 控制结构
- 领域建模
- 方法
- 头等函数
- 单例对象
- 集合
- 上下文抽象
- 顶层定义
- 总结
- 类型初探
- 字符串插值
- 控制结构
- 领域建模
- 工具
- OOP 领域建模
- 函数式领域建模
- 方法
- 方法特性
- main 方法
- 总结
- 函数
- 匿名函数
- 函数变量
- Eta 扩展
- 高阶函数
- 自定义 map 函数
- 创建可以返回函数的方法
- 总结
- 打包和导入
- Scala 集合
- 集合类型
- 集合方法
- 总结
- 函数式编程
- 什么是函数式编程?
- 不可变值
- 纯函数
- 函数是值
- 函数式错误处理
- 总结
- 类型和类型系统
- 类型推断
- 泛型
- 相交类型
- 联合类型
- 代数数据类型
- 型变
- 不透明类型
- 结构化类型
- 依赖函数类型
- 其他类型
- 上下文抽象
- 扩展方法
- Given 实例和 Using 语句
- 上下文绑定
- Given 导入
- 实现类型类
- 多元相等性
- 隐式转换
- 总结
- 并发
- Scala 工具
- 使用 sbt 构建和测试 Scala 项目
- worksheet
- 与 Java 交互
- 向 Java 开发者介绍Scala
- Scala for JavaScript Developers
- Scala for Python Developers
- 下一步去哪