值類別和通用特質

語言
此文件頁面特定於 Scala 2 中發布的功能,這些功能已在 Scala 3 中移除或由替代方案取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用的是 Scala 2。

在 Scala 3 中,值類別仍受支援以維持相容性,但建議使用 不透明類型 來達成相同的結果。

簡介

最初在 SIP-15 中提出,並在 Scala 2.10.0 中引入,值類別是 Scala 中的一種機制,用於避免配置執行時期物件。這是透過定義新的 AnyVal 子類別來完成的。

以下顯示非常小的值類別定義

class Wrapper(val underlying: Int) extends AnyVal

它有一個單一的公開 val 參數,這是底層執行時間表示。編譯時的類型是 Wrapper,但在執行時間,表示是一個 Int。值類別可以定義 def,但不能定義 valvar 或巢狀 traitclassobject

class Wrapper(val underlying: Int) extends AnyVal {
  def foo: Wrapper = new Wrapper(underlying * 19)
}

值類別只能延伸 *通用特質*,且本身不能被延伸。*通用特質* 是一個延伸 Any 的特質,只有 def 作為成員,且不進行初始化。通用特質允許基本繼承值類別的方法,但它們 *會產生配置的開銷*。例如,

trait Printable extends Any {
  def print(): Unit = println(this)
}
class Wrapper(val underlying: Int) extends AnyVal with Printable

val w = new Wrapper(3)
w.print() // actually requires instantiating a Wrapper instance

本文件說明的其餘部分顯示使用案例、配置何時發生和不發生的詳細資料,以及值類別限制的具體範例。

擴充方法

值類別的一個使用案例是將它們與隱式類別 (SIP-13) 結合,以提供不配置的擴充方法。使用隱式類別提供更方便的語法來定義擴充方法,而值類別則移除執行時間開銷。一個很好的範例是標準函式庫中的 RichInt 類別。RichInt 使用多個方法延伸 Int 類型。因為它是一個值類別,所以使用 RichInt 方法時不需要建立 RichInt 的執行個體。

以下 RichInt 片段顯示它如何擴充 Int 以允許表達式 3.toHexString

implicit class RichInt(val self: Int) extends AnyVal {
  def toHexString: String = java.lang.Integer.toHexString(self)
}

在執行階段,此表達式 3.toHexString 最佳化為對靜態物件的方法呼叫(RichInt$.MODULE$.toHexString$extension(3)),而不是對新實體化物件的方法呼叫。

正確性

值類別的另一個使用案例是取得資料類型的類型安全性,而沒有執行階段配置的開銷。例如,表示距離的資料類型片段可能如下所示

class Meter(val value: Double) extends AnyVal {
  def +(m: Meter): Meter = new Meter(value + m.value)
}

新增兩個距離的程式碼,例如

val x = new Meter(3.4)
val y = new Meter(4.3)
val z = x + y

實際上不會配置任何 Meter 實體,而只會在執行階段使用原始 double。

注意:實際上,你可以使用案例類別和/或擴充方法來獲得更簡潔的語法。

何時需要配置

由於 JVM 不支援值類別,因此 Scala 有時需要實際實體化值類別。可以在 SIP-15 中找到完整詳細資料。

配置摘要

值類別實際上會在以下情況實體化

  1. 值類別被視為另一種類型。
  2. 值類別被指定給陣列。
  3. 進行執行階段類型測試,例如模式配對。

配置詳細資料

每當值類別被視為另一種類型,包括通用特質,都必須實體化實際值類別的實體。例如,考量 Meter 值類別

trait Distance extends Any
case class Meter(value: Double) extends AnyVal with Distance

接受 Distance 類型值的函式將需要實際 Meter 實體。在以下範例中,Meter 類別實際上會實體化

def add(a: Distance, b: Distance): Distance = ...
add(Meter(3.4), Meter(4.3))

如果 add 的簽章改為

def add(a: Meter, b: Meter): Meter = ...

那麼就不需要配置。這個規則的另一個範例是當值類別用作類型引數時。例如,即使呼叫 identity,也必須建立實際的 Meter 執行個體

def identity[T](t: T): T = t
identity(Meter(5.0))

另一個需要配置的情況是在指定陣列時,即使它是該值類別的陣列。例如,

val m = Meter(5.0)
val array = Array[Meter](m)

此處的陣列包含實際的 Meter 執行個體,而不仅仅是基礎的 double 原生型別。

最後,在模式比對或 asInstanceOf 中執行的類型測試需要實際的值類別執行個體

case class P(i: Int) extends AnyVal

val p = P(3)
p match { // new P instantiated here
  case P(3) => println("Matched 3")
  case P(_) => println("Not 3")
}

限制

值類別目前有幾個限制,部分原因是 JVM 本身不支援值類別的概念。可以在 SIP-15 中找到值類別實作及其限制的完整詳細資料。

限制摘要

值類別...

  1. ... 只能有一個主建構函式,且只有一個公開的 val 參數,其類型不是使用者定義的值類別。(從 Scala 2.11.0 開始,參數可以是非公開的。)
  2. ... 不能有 @specialized 類型參數。
  3. ... 不能有巢狀或區域類別、特質或物件。
  4. ... 不能定義具體的 equalshashCode 方法。
  5. ... 必須是頂層類別或靜態可存取物件的成員。
  6. ... 成員只能是 def。特別是,它不能有 lazy val、var 或 val 作為成員。
  7. …無法由其他類別擴充。

限制範例

本節提供許多在前一節已說明限制的具體範例。

不允許多個建構函數參數

class Complex(val real: Double, val imag: Double) extends AnyVal

且 Scala 編譯器會產生下列錯誤訊息

Complex.scala:1: error: value class needs to have exactly one public val parameter
class Complex(val real: Double, val imag: Double) extends AnyVal
      ^

由於建構函數參數必須是 val,因此無法是依名稱參數

NoByName.scala:1: error: `val` parameters may not be call-by-name
class NoByName(val x: => Int) extends AnyVal
                      ^

Scala 不允許延遲 val 建構函數參數,因此也不允許。不允許多個建構函數

class Secondary(val x: Int) extends AnyVal {
  def this(y: Double) = this(y.toInt)
}

Secondary.scala:2: error: value class may not have secondary constructors
  def this(y: Double) = this(y.toInt)
      ^

值類別無法有延遲 val、var 或 val 作為成員,且無法有巢狀類別、特質或物件

class NoLazyMember(val evaluate: () => Double) extends AnyVal {
  val member: Int = 3
  var y: Int = 4
  lazy val x: Double = evaluate()
  object NestedObject
  class NestedClass
}

Invalid.scala:2: error: this statement is not allowed in value class: val member: Int = 3
  val member: Int = 3
      ^
Invalid.scala:3: error: this statement is not allowed in value class: var y: Int = 4
  var y: Int = 4
      ^
Invalid.scala:4: error: this statement is not allowed in value class: lazy val x: Double = NoLazyMember.this.evaluate.apply()
  lazy val x: Double = evaluate()
           ^
Invalid.scala:5: error: value class may not have nested module definitions
  object NestedObject
         ^
Invalid.scala:6: error: value class may not have nested class definitions
  class NestedClass
        ^

請注意,也不允許局部類別、特質和物件,如下所示

class NoLocalTemplates(val x: Int) extends AnyVal {
  def aMethod = {
    class Local
    ...
  }
}

Local.scala:3: error: implementation restriction: nested class is not allowed in value class
  class Local
        ^

目前的實作限制是值類別無法巢狀

class Outer(val inner: Inner) extends AnyVal
class Inner(val value: Int) extends AnyVal

Nested.scala:1: error: value class may not wrap another user-defined value class
class Outer(val inner: Inner) extends AnyVal
                ^

此外,結構化類型無法在方法參數或回傳類型中使用值類別

class Value(val x: Int) extends AnyVal
object Usage {
  def anyValue(v: { def value: Value }): Value =
    v.value
}

Struct.scala:3: error: Result type in structural refinement may not refer to a user-defined value class
  def anyValue(v: { def value: Value }): Value =
                               ^

值類別可能無法擴充非通用特質,且值類別本身無法被擴充

trait NotUniversal
class Value(val x: Int) extends AnyVal with NotUniversal
class Extend(x: Int) extends Value(x)

Extend.scala:2: error: illegal inheritance; superclass AnyVal
 is not a subclass of the superclass Object
 of the mixin trait NotUniversal
class Value(val x: Int) extends AnyVal with NotUniversal
                                            ^
Extend.scala:3: error: illegal inheritance from final class Value
class Extend(x: Int) extends Value(x)
                             ^

第二個錯誤訊息顯示,儘管未明確指定 final 修飾詞給值類別,但假設已指定。

另一個限制是只支援類別的一個參數,因此值類別必須是頂層或靜態可存取物件的成員。這是因為巢狀值類別需要第二個參數來參照封裝類別。因此,這是不允許的

class Outer {
  class Inner(val x: Int) extends AnyVal
}

Outer.scala:2: error: value class may not be a member of another class
class Inner(val x: Int) extends AnyVal
      ^

但這是允許的,因為封裝物件是頂層

object Outer {
  class Inner(val x: Int) extends AnyVal
}

此頁面的貢獻者