在GitHub上編輯此頁面

擴充方法

擴充方法允許在定義類型後將方法新增到類型。範例

case class Circle(x: Double, y: Double, radius: Double)

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2

與一般方法一樣,擴充方法可以用中綴.呼叫

val circle = Circle(0, 0, 1)
circle.circumference

擴充方法的翻譯

擴充方法會轉換為一個特別標記的方法,它會將領先的參數區段作為其第一個參數清單。標記在此表示為 <extension>,是編譯器內部的。因此,上述 circumference 的定義會轉換為下列方法,且也可以這樣呼叫

<extension> def circumference(c: Circle): Double = c.radius * math.Pi * 2

assert(circle.circumference == circumference(circle))

運算子

擴充方法語法也可以用來定義運算子。範例

extension (x: String)
  def < (y: String): Boolean = ...
extension (x: Elem)
  def +: (xs: Seq[Elem]): Seq[Elem] = ...
extension (x: Number)
  infix def min (y: Number): Number = ...

"ab" < "c"
1 +: List(2, 3)
x min 3

上述三個定義會轉換為

<extension> def < (x: String)(y: String): Boolean = ...
<extension> def +: (xs: Seq[Elem])(x: Elem): Seq[Elem] = ...
<extension> infix def min(x: Number)(y: Number): Number = ...

請注意,在將右結合運算子 +: 轉換為擴充方法時,兩個參數 xxs 會互換。這類似於將右結合運算子實作為一般方法。Scala 編譯器會將中綴運算 x +: xs 預處理為 xs.+:(x),因此擴充方法最後會套用在序列上,作為第一個參數(換句話說,兩個互換會互相抵消)。請參閱此處以取得詳細資料。

泛型擴充

也可以透過新增型別參數到擴充來擴充泛型型別。例如

extension [T](xs: List[T])
  def second = xs.tail.head

extension [T: Numeric](x: T)
  def + (y: T): T = summon[Numeric[T]].plus(x, y)

擴充上的型別參數也可以與方法本身上的型別參數結合

extension [T](xs: List[T])
  def sumBy[U: Numeric](f: T => U): U = ...

與方法型別參數相符的型別引數會以一般方式傳遞

List("a", "bb", "ccc").sumBy[Int](_.length)

相反地,與 extension 之後型別參數相符的型別引數只能在將方法視為非擴充方法時傳遞

sumBy[String](List("a", "bb", "ccc"))(_.length)

或者,在傳遞兩個型別引數時

sumBy[String](List("a", "bb", "ccc"))[Int](_.length)

擴充也可以使用 using 子句。例如,上述的 + 擴充也可以用 using 子句寫成等效的

extension [T](x: T)(using n: Numeric[T])
  def + (y: T): T = n.plus(x, y)

集合擴充

有時,我們想要定義多個擴充方法,它們共用相同的左端參數型別。在這種情況下,我們可以將共用參數「拉出」到單一擴充,並將所有方法括在花括號或縮排區域中。範例

extension (ss: Seq[String])

  def longestStrings: Seq[String] =
    val maxLength = ss.map(_.length).max
    ss.filter(_.length == maxLength)

  def longestString: String = longestStrings.head

相同的內容可以用花括號寫成如下(請注意,縮排區域仍可以在花括號內使用)

extension (ss: Seq[String]) {

  def longestStrings: Seq[String] = {
    val maxLength = ss.map(_.length).max
    ss.filter(_.length == maxLength)
  }

  def longestString: String = longestStrings.head
}

請注意 longestString 的右端:它直接呼叫 longestStrings,隱含地假設共用的擴充值 ss 為接收器。

像這樣的集合擴充是單獨擴充的簡寫,其中每個方法都個別定義。例如,上述的第一個擴充會擴充為

extension (ss: Seq[String])
  def longestStrings: Seq[String] =
    val maxLength = ss.map(_.length).max
    ss.filter(_.length == maxLength)

extension (ss: Seq[String])
  def longestString: String = ss.longestStrings.head

集合擴充也可以使用型別參數和 using 子句。範例

extension [T](xs: List[T])(using Ordering[T])
  def smallest(n: Int): List[T] = xs.sorted.take(n)
  def smallestIndices(n: Int): List[Int] =
    val limit = smallest(n).max
    xs.zipWithIndex.collect { case (x, i) if x <= limit => i }

擴充方法呼叫的翻譯

若要轉換對擴充方法的參照,編譯器必須知道擴充方法。我們在此情況下會說,擴充方法在參照點是適用的。擴充方法有四種可能的適用方式

  1. 擴充方法在一個簡單的名稱下可見,透過在包含參考的範圍中定義、繼承或匯入。
  2. 擴充方法是某個給定實例的成員,在參考點可見。
  3. 參考的形式為 r.m,而擴充方法在 r 類型的隱含範圍中定義。
  4. 參考的形式為 r.m,而擴充方法在 r 類型的隱含範圍中的某個給定實例中定義。

以下是第一個規則的範例

trait IntOps:
  extension (i: Int) def isZero: Boolean = i == 0

  extension (i: Int) def safeMod(x: Int): Option[Int] =
    // extension method defined in same scope IntOps
    if x.isZero then None
    else Some(i % x)

object IntOpsEx extends IntOps:
  extension (i: Int) def safeDiv(x: Int): Option[Int] =
    // extension method brought into scope via inheritance from IntOps
    if x.isZero then None
    else Some(i / x)

trait SafeDiv:
  import IntOpsEx.* // brings safeDiv and safeMod into scope

  extension (i: Int) def divide(d: Int): Option[(Int, Int)] =
    // extension methods imported and thus in scope
    (i.safeDiv(d), i.safeMod(d)) match
      case (Some(d), Some(r)) => Some((d, r))
      case _ => None

根據第二個規則,可透過定義包含它的給定實例來提供擴充方法,如下所示

given ops1: IntOps()  // brings safeMod into scope

1.safeMod(2)

根據第三和第四個規則,如果擴充方法在接收者類型的隱含範圍或該範圍中的給定實例中,則可使用。範例

class List[T]:
  ...
object List:
  ...
  extension [T](xs: List[List[T]])
    def flatten: List[T] = xs.foldLeft(List.empty[T])(_ ++ _)

  given [T: Ordering]: Ordering[List[T]] with
    extension (xs: List[T])
      def < (ys: List[T]): Boolean = ...
end List

// extension method available since it is in the implicit scope
// of List[List[Int]]
List(List(1, 2), List(3, 4)).flatten

// extension method available since it is in the given Ordering[List[T]],
// which is itself in the implicit scope of List[Int]
List(1, 2) < List(3)

解析選擇為擴充方法的精確規則如下。

假設選擇 e.m[Ts],其中 m 不是 e 的成員,類型引數 [Ts] 是選用的,且 T 是預期的類型。依序嘗試下列兩個改寫

  1. 選擇改寫為 m[Ts](e),並使用下列名稱解析規則的輕微修改進行類型檢查
  • 如果 m 是由所有在巢狀層級上的多個匯入匯入的,請嘗試每個匯入作為擴充方法,而不是因歧義而失敗。如果只有一個匯入導致擴充在沒有錯誤的情況下通過類型檢查,請選擇該擴充。如果有幾個這樣的匯入,但只有一個匯入不是萬用字元匯入,請從該匯入中選擇擴充。否則,報告歧義參考錯誤。

    注意:此匯入規則放寬僅適用於將方法 m 用作擴充方法的情況。如果在字首形式中將其用作一般方法,則套用一般匯入規則,這表示從多個地方匯入 m 可能導致歧義錯誤。

  1. 如果第一次改寫未通過預期類型 T 的類型檢查,且在某些符合資格的物件 o 中有擴充方法 m,則選擇改寫為 o.m[Ts](e)。物件 o 符合資格,如果

    • o 形成 T 的隱含範圍的一部分,或
    • o 是在應用程式點可見的給定執行個體,或
    • oT 的隱含範圍中的給定執行個體。

    這個第二次重寫嘗試在編譯器同時嘗試從 T 進行隱含轉換至包含 m 的類型時進行。如果有多種重寫方式,則會產生歧義錯誤。

延伸方法也可以使用沒有前置表達式的簡單識別碼來參照。如果識別碼 g 出現在延伸方法 f 的主體中,並參照在相同集合延伸中定義的延伸方法 g

extension (x: T)
  def f ... = ... g ...
  def g ...

識別碼會重寫為 x.g。如果 fg 是相同方法,也是如此。範例

extension (s: String)
  def position(ch: Char, n: Int): Int =
    if n < s.length && s(n) != ch then position(ch, n + 1)
    else n

遞迴呼叫 position(ch, n + 1) 在此情況會擴充為 s.position(ch, n + 1)。整個延伸方法會重寫為

def position(s: String)(ch: Char, n: Int): Int =
  if n < s.length && s(n) != ch then position(s)(ch, n + 1)
  else n

語法

以下是延伸方法和集合延伸相對於 目前語法 的語法變更。

BlockStat         ::=  ... | Extension
TemplateStat      ::=  ... | Extension
TopStat           ::=  ... | Extension
Extension         ::=  ‘extension’ [DefTypeParamClause] {UsingParamClause}
                       ‘(’ DefParam ‘)’ {UsingParamClause} ExtMethods
ExtMethods        ::=  ExtMethod | [nl] <<< ExtMethod {semi ExtMethod} >>>
ExtMethod         ::=  {Annotation [nl]} {Modifier} ‘def’ DefDef

在上述 ExtMethods 產生規則中的符號 <<< ts >>> 定義如下

<<< ts >>>        ::=  ‘{’ ts ‘}’ | indent ts outdent

extension 是軟關鍵字。它僅在出現在陳述句的開頭且後接 [( 時才被辨識為關鍵字。在所有其他情況下,它會被視為識別碼。