擴充方法
擴充方法允許在定義類型後將方法新增到類型。範例
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 = ...
請注意,在將右結合運算子 +:
轉換為擴充方法時,兩個參數 x
和 xs
會互換。這類似於將右結合運算子實作為一般方法。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 }
擴充方法呼叫的翻譯
若要轉換對擴充方法的參照,編譯器必須知道擴充方法。我們在此情況下會說,擴充方法在參照點是適用的。擴充方法有四種可能的適用方式
- 擴充方法在一個簡單的名稱下可見,透過在包含參考的範圍中定義、繼承或匯入。
- 擴充方法是某個給定實例的成員,在參考點可見。
- 參考的形式為
r.m
,而擴充方法在r
類型的隱含範圍中定義。 - 參考的形式為
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
是預期的類型。依序嘗試下列兩個改寫
- 選擇改寫為
m[Ts](e)
,並使用下列名稱解析規則的輕微修改進行類型檢查
-
如果
m
是由所有在巢狀層級上的多個匯入匯入的,請嘗試每個匯入作為擴充方法,而不是因歧義而失敗。如果只有一個匯入導致擴充在沒有錯誤的情況下通過類型檢查,請選擇該擴充。如果有幾個這樣的匯入,但只有一個匯入不是萬用字元匯入,請從該匯入中選擇擴充。否則,報告歧義參考錯誤。注意:此匯入規則放寬僅適用於將方法
m
用作擴充方法的情況。如果在字首形式中將其用作一般方法,則套用一般匯入規則,這表示從多個地方匯入m
可能導致歧義錯誤。
-
如果第一次改寫未通過預期類型
T
的類型檢查,且在某些符合資格的物件o
中有擴充方法m
,則選擇改寫為o.m[Ts](e)
。物件o
符合資格,如果o
形成T
的隱含範圍的一部分,或o
是在應用程式點可見的給定執行個體,或o
是T
的隱含範圍中的給定執行個體。
這個第二次重寫嘗試在編譯器同時嘗試從
T
進行隱含轉換至包含m
的類型時進行。如果有多種重寫方式,則會產生歧義錯誤。
延伸方法也可以使用沒有前置表達式的簡單識別碼來參照。如果識別碼 g
出現在延伸方法 f
的主體中,並參照在相同集合延伸中定義的延伸方法 g
extension (x: T)
def f ... = ... g ...
def g ...
識別碼會重寫為 x.g
。如果 f
和 g
是相同方法,也是如此。範例
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
是軟關鍵字。它僅在出現在陳述句的開頭且後接 [
或 (
時才被辨識為關鍵字。在所有其他情況下,它會被視為識別碼。