Link Search Menu Expand Document

语法

语法上,并集类型遵循与交集类型相同的规则,但优先级较低,请参见交集类型 - 更多细节

与模式匹配语法的交互

| 也用于在模式匹配中分割 pattern alternatives,并且其优先级低于用于类型模式中的 :,这意味着:

case _: A | B => ...

等价于:

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

而不同于:

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

子类型规则

  • 对于任意类型 ABA 总是 A | B 的子类型。
  • 如果 A <: C 并且 B <: C,则 A | B <: C
  • 类似于 &| 满足交换律和结合律:

    A | B =:= B | A
    A | (B | C) =:= (A | B) | C
    
  • & is distributive over |:

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

根据这些规则,一组类型的LUB(最小上限)是这些类型的并集。这替代了 Scala 2 规范中的最小上限定义

动机

The primary reason for introducing union types in Scala is that they allow us to guarantee that for every set of types, we can always form a finite LUB. This is both useful in practice (infinite LUBs in Scala 2 were approximated in an ad-hoc way, resulting in imprecise and sometimes incredibly long types) and in theory (the type system of Scala 3 is based on the DOT calculus, which has union types).

Additionally, union types are a useful construct when trying to give types to existing dynamically typed APIs, this is why they’re an integral part of TypeScript and have even been partially implemented in Scala.js.

Join of a union type

In some situation described below, a union type might need to be widened to a non-union type, for this purpose we define the join of a union type T1 | ... | Tn as the smallest intersection type of base class instances of T1,…,Tn. Note that union types might still appear as type arguments in the resulting type, this guarantees that the join is always finite.

Example

Given

trait C[+T]
trait D
trait E
class A extends C[A] with D
class B extends C[B] with D with E

The join of A | B is C[A | B] & D

Type inference

When inferring the result type of a definition (val, var, or def) and the type we are about to infer is a union type, then we replace it by its join. Similarly, when instantiating a type argument, if the corresponding type parameter is not upper-bounded by a union type and the type we are about to instantiate is a union type, we replace it by its join. This mirrors the treatment of singleton types which are also widened to their underlying type unless explicitly specified. The motivation is the same: inferring types which are “too precise” can lead to unintuitive typechecking issues later on.

Note: Since this behavior limits the usability of union types, it might be changed in the future. For example by not widening unions that have been explicitly written down by the user and not inferred, or by not widening a type argument when the corresponding type parameter is covariant.

See PR #2330 and Issue #4867 for further discussions.

Example

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

This code typechecks because the inferred type argument to ListBuffer in the right-hand side of x was Left[Int, Nothing] | Right[Nothing, String] which was widened to Either[Int, String]. If the compiler hadn’t done this widening, the last line wouldn’t typecheck because ListBuffer is invariant in its argument.

Members

The members of a union type are the members of its join.

Example

The following code does not typecheck, because method hello is not a member of AnyRef which is the join of A | 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

On the other hand, the following would be allowed

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

Exhaustivity checking

If the selector of a pattern match is a union type, the match is considered exhaustive if all parts of the union are covered.

Erasure

The erased type for A | B is the erased least upper bound of the erased types of A and B. Quoting from the documentation of TypeErasure#erasedLub, the erased LUB is computed as follows:

  • if both argument are arrays of objects, an array of the erased LUB of the element types
  • if both arguments are arrays of same primitives, an array of this primitive
  • if one argument is array of primitives and the other is array of objects, Object
  • if one argument is an array, Object
  • otherwise a common superclass or trait S of the argument classes, with the following two properties:
    • S is minimal: no other common superclass or trait derives from S
    • S is last : in the linearization of the first argument type |A| there are no minimal common superclasses or traits that come after S. The reason to pick last is that we prefer classes over traits that way, which leads to more predictable bytecode and (?) faster dynamic dispatch.