在 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 & X,而 A | 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 方法不是 A | B 的聯集 AnyRef 的成員。

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 的陣列
  • 如果兩個參數都是相同基本類型的陣列,則為此基本類型的陣列
  • 如果一個參數是基本類型的陣列,而另一個是物件陣列,則為 物件
  • 如果一個參數是陣列,則為 物件
  • 否則為參數類別的共同超類別或特徵 S,具有以下兩個屬性
    • S 為最小:沒有其他共同超類別或特徵衍生自 S
    • S 為最後:在第一個參數類型 |A| 的線性化中,沒有位於 S 之後的最小共同超類別或特徵。選擇最後的原因是,我們偏好類別而非特徵,這會產生更可預測的位元組碼和 (?) 更快的動態調度。