在 GitHub 上编辑此页面

联合类型 - 更多细节

语法

从语法上讲,联合遵循与交集相同的规则,但优先级较低,请参阅 交集类型 - 更多细节.

与模式匹配语法的交互

| 也用于模式匹配以分隔模式备选方案,并且优先级低于 :(在类型化模式中使用),这意味着

case _: A | B => ...

仍然等效于

case (_: A) | B => ...

而不是

case _: (A | B) => ...

子类型规则

  • 对于所有 ABA 始终是 A | B 的子类型。

  • 如果 A <: CB <: C,则 A | B <: C

  • & 类似,| 是可交换的和可结合的

    A | B =:= B | A
    A | (B | C) =:= (A | B) | C
    
  • &| 是分配的

    A & (B | C) =:= A & B | A & C
    

从这些规则可以得出,一组类型的最小上界 (LUB) 是这些类型的并集。这取代了 Scala 2 规范中最小上界的定义

动机

在 Scala 中引入联合类型的首要原因是,它们允许我们保证对于每组类型,我们始终可以形成一个有限的 LUB。这在实践中(Scala 2 中的无限 LUB 以一种特殊的方式近似,导致类型不精确,有时会非常长)和理论上(Scala 3 的类型系统基于 DOT 演算,它具有联合类型)都是有用的。

此外,联合类型在尝试为现有的动态类型 API 提供类型时是一个有用的结构,这就是为什么它们是 TypeScript 的组成部分,甚至在 Scala.js 中部分实现

联合类型的连接

在下面描述的某些情况下,可能需要将联合类型扩展为非联合类型,为此,我们将联合类型 T1 | ... | Tn连接定义为 T1、...、Tn 的基类实例的最小交集类型。请注意,联合类型可能仍然作为类型参数出现在结果类型中,这保证了连接始终是有限的。

联合类型的可见连接是其连接,其中所有作为 透明 特性或类的实例的交集操作数都被删除。

示例

给定

trait C[+T]
trait D
trait E
transparent trait X
class A extends C[A], D, X
class B extends C[B], D, E, X

A | B 的连接是 C[A | B] & D & XA | B 的可见连接是 C[A | B] & D

硬联合类型和软联合类型

我们区分硬联合类型和软联合类型。联合类型是在源代码中显式编写的联合类型。例如,在

val x: Int | String = ...

Int | String 将是一个硬联合类型。联合类型是由于对表达式备选方案进行类型检查而产生的类型。例如,表达式的类型

val x = 1
val y = "abc"
if cond then x else y

是软联合类型 Int | String。匹配表达式也是如此。表达式的类型

x match
  case 1 => x
  case 2 => "abc"
  case 3 => List(1, 2, 3)

是软联合类型 Int | "abc" | List[Int]

类型推断

在推断定义(valvardef)的结果类型时,如果要推断的类型是软联合类型,则将其替换为其可见连接,前提是它不为空。类似地,在实例化类型参数时,如果相应的类型参数没有被联合类型上界限制,并且要实例化的类型是软联合类型,则将其替换为其可见连接,前提是它不为空。这反映了对单例类型的处理,除非明确指定,否则它们也会被扩展到其底层类型。其动机相同:推断“过于精确”的类型可能会导致以后出现不直观的类型检查问题。

示例

import scala.collection.mutable.ListBuffer
val x = ListBuffer(Right("foo"), Left(0))
val y: ListBuffer[Either[Int, String]] = x

这段代码可以进行类型检查,因为推断出的 x 右侧 ListBuffer 的类型参数是 Left[Int, Nothing] | Right[Nothing, String],它被扩展为 Either[Int, String]。如果编译器没有进行这种扩展,最后一行将无法进行类型检查,因为 ListBuffer 对其参数是不变的。

成员

联合类型的成员是其连接的成员。

示例

以下代码无法进行类型检查,因为方法 hello 不是 AnyRef 的成员,而 AnyRefA | B 的连接。

trait A { def hello: String }
trait B { def hello: String }

def test(x: A | B) = x.hello // error: value `hello` is not a member of A | B

另一方面,以下代码将被允许

trait C { def hello: String }
trait A extends C with D
trait B extends C with E

def test(x: A | B) = x.hello // ok as `hello` is a member of the join of A | B which is C

穷举检查

如果模式匹配的选择器是联合类型,则如果覆盖了联合的所有部分,则匹配被认为是穷举的。

擦除

A | B 的擦除类型是 AB 的擦除类型的擦除最小上界。引用 TypeErasure#erasedLub 文档,擦除 LUB 的计算方式如下

  • 如果两个参数都是对象数组,则为元素类型的擦除 LUB 的数组
  • 如果两个参数都是相同类型的基本类型数组,则为该基本类型的数组
  • 如果一个参数是基本类型数组,另一个参数是对象数组,则为 Object
  • 如果一个参数是数组,则为 Object
  • 否则,参数类的公共超类或特征 S,具有以下两个属性
    • S 是最小的:没有其他公共超类或特征从 S 派生
    • S 是最后一个:在第一个参数类型 |A| 的线性化中,没有出现在 S 之后的最小公共超类或特征。选择最后一个的原因是,我们更喜欢类而不是特征,这将导致更可预测的字节码和 (?) 更快的动态调度。