枚舉和 ADT 的翻譯
編譯器會將列舉及其案例擴充為僅使用 Scala 其他語言功能的程式碼。因此,Scala 中的列舉是方便的語法糖,但它們並非理解 Scala 核心所必需的。
我們現在詳細說明列舉的擴充。首先,一些術語和符號慣例
-
我們使用
E
作為列舉的名稱,並使用C
作為出現在E
中的案例的名稱。 -
我們使用
<...>
表示在某些情況下可能為空的語法結構。例如,<value-params>
表示一個或多個參數清單(...)
或完全沒有。 -
列舉案例分為三類
- 類別案例是那些帶有參數化的案例,它們帶有型別參數區段
[...]
或一個或多個(可能為空)參數區段(...)
。 - 簡單案例是非泛型列舉的案例,它們既沒有參數,也沒有 extends 子句或主體。也就是說,它們只包含一個名稱。
- 值案例是不具有參數區段但具有(可能產生的)
extends
子句和/或主體的所有案例。
- 類別案例是那些帶有參數化的案例,它們帶有型別參數區段
簡單案例和值案例統稱為單例案例。
去糖規則暗示類別案例會對應到案例類別,而單例案例會對應到 val
定義。
有九個去糖規則。規則 (1) 去除列舉定義的糖。規則 (2) 和 (3) 去除簡單案例的糖。規則 (4) 到 (6) 定義缺少 extends
子句的案例的 extends
子句。規則 (7) 到 (9) 定義具有 extends
子句的案例如何對應到 case class
或 val
。
-
一個
enum
定義enum E ... { <defs> <cases> }
會擴充到一個
sealed abstract
類別,它會延伸scala.reflect.Enum
特質和一個相關的伴隨物件,其中包含已定義的案例,並根據規則 (2 - 8) 進行擴充。列舉類別會從一個編譯器產生的匯入開始,匯入所有案例的名稱<caseIds>
,以便可以在類別中不加前綴使用這些名稱。sealed abstract class E ... extends <parents> with scala.reflect.Enum { import E.{ <caseIds> } <defs> } object E { <cases> }
-
一個由逗號分隔的列舉名稱清單組成的簡單案例
case C_1, ..., C_n
會擴充到
case C_1; ...; case C_n
原始案例上的任何修飾詞或註解都會擴充到所有已擴充的案例。
-
一個簡單案例
case C
一個沒有使用型別參數的列舉
E
會擴充到val C = $new(n, "C")
這裡,
$new
是建立一個E
執行個體的私人方法(見下文)。 -
如果
E
是有型別參數的列舉V1 T1 >: L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0)
其中每個變異
Vi
都是'+'
或'-'
,則一個簡單案例case C
會擴充到
case C extends E[B1, ..., Bn]
其中
Bi
是Li
(如果Vi = '+'
)和Ui
(如果Vi = '-'
)。然後,這個結果會進一步用規則 (8) 重新寫入。不允許有非變異型別參數的列舉的簡單案例(但是,具有明確extends
子句的值案例是允許的) -
一個沒有 extends 子句的類別案例
case C <type-params> <value-params>
一個沒有使用型別參數的列舉
E
會擴充到case C <type-params> <value-params> extends E
此結果接著使用規則 (9) 重新寫入。
-
如果
E
是具有類型參數Ts
的列舉,則為沒有類型參數且沒有延伸子句的類別案例case C <value-params>
會擴充到
case C[Ts] <value-params> extends E[Ts]
此結果接著使用規則 (9) 重新寫入。對於具有類型參數的類別案例,需要明確提供延伸子句。
-
如果
E
是具有類型參數Ts
的列舉,則為沒有類型參數但具有延伸子句的類別案例case C <value-params> extends <parents>
會擴充到
case C[Ts] <value-params> extends <parents>
前提是至少一個參數
Ts
在<value-params>
的參數類型或<parents>
的類型引數中被提及。 -
值案例
case C extends <parents>
擴充為
E
的伴隨物件中的值定義val C = new <parents> { <body>; def ordinal = n }
其中
n
是伴隨物件中案例的序號,從 0 開始。匿名類別也實作它從Enum
繼承的抽象Product
方法。如果值案例在
<parents>
的類型引數中參照封閉enum
的類型參數,則會發生錯誤。 -
類別案例
case C <params> extends <parents>
擴充類似於
E
的伴隨物件中的最終案例類別final case class C <params> extends <parents>
列舉案例定義以下形式的
ordinal
方法def ordinal = n
其中
n
是伴隨物件中案例的序號,從 0 開始。如果值案例在
<params>
的參數類型或<parents>
的類型引數中參照封閉enum
的類型參數,則會發生錯誤,除非該參數已經是案例的類型參數,亦即參數名稱定義在<params>
中。編譯器產生的列舉案例的
apply
和copy
方法case C(ps) extends P1, ..., Pn
會特別處理。應用方法的呼叫
C(ts)
會被指定底層類型P1 & ... & Pn
(捨棄任何 透明特質),只要該類型在應用點仍與預期類型相容。C
的copy
方法呼叫t.copy(ts)
會以相同方式處理。
單例案例列舉的翻譯
定義一個或多個單例案例的列舉 E
(可能是泛型的)會在其伴隨物件中定義下列附加合成成員(其中 E'
表示 E
,其任何類型參數都已替換為萬用字元)
- 方法
valueOf(name: String): E'
。它會傳回識別碼為name
的單例案例值。 - 方法
values
,它會傳回E
定義的所有單例案例值的Array[E']
,依其定義順序。
如果 E
包含至少一個簡單案例,其伴隨物件會另外定義
-
私有方法
$new
,它會定義一個新的簡單案例值,其序號和名稱已給定。此方法可以視為已定義如下。private def $new(_$ordinal: Int, $name: String) = new E with runtime.EnumValue: def ordinal = _$ordinal override def productPrefix = $name // if not overridden in `E` override def toString = $name // if not overridden in `E`
匿名類別也實作了它從 Enum
繼承的抽象 Product
方法。僅當列舉未從 java.lang.Enum
延伸(因為 Scala 列舉不會延伸 java.lang.Enum
,除非明確指定)時,才會產生 ordinal
方法。如果它確實延伸,則不需要產生 ordinal
,因為 java.lang.Enum
已定義它。類似地,不需要覆寫 toString
,因為它已在 java.lang.Enum
中根據 name
定義。最後,當 E
延伸 java.lang.Enum
時,productPrefix
會呼叫 this.name
。
列舉案例的範圍
enum
中的案例會以類似於次要建構函數的方式處理。它無法使用 this
存取封裝的 enum
,也無法使用簡單識別碼存取其值參數或實例成員。
即使已翻譯的列舉案例位於列舉的伴隨物件中,透過 this
或簡單識別碼參照此物件或其成員也是不允許的。編譯器會在封裝伴隨物件的範圍中類型檢查列舉案例,但會將任何此類非法存取標記為錯誤。
與 Java 相容的列舉的翻譯
與 Java 相容的列舉是延伸 java.lang.Enum
的列舉。翻譯規則與上述相同,但有本節定義的保留事項。
與 Java 相容的列舉具有類別案例是編譯時期錯誤。
例如 case C
的案例會擴充為 @static val
,而不是 val
。這允許它們作為列舉類型的靜態欄位產生,從而確保它們的表示方式與 Java 列舉相同。
其他規則
-
未從列舉案例產生的常態案例類別不允許延伸
scala.reflect.Enum
。這可確保列舉的唯一案例是明確宣告在其中的案例。 -
如果列舉案例有
extends
子句,列舉類別必須是延伸的類別之一。