在 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 是一个软关键字。仅当它出现在语句开头后跟 [( 时,才被识别为关键字。在所有其他情况下,它都被视为标识符。