在 GitHub 上编辑此页面

运算符规则

中缀运算符的规则在某些部分发生了变化

首先,字母数字方法只能在定义中带有 infix 修饰符时才能用作中缀运算符。

其次,建议(但不是强制)使用 @targetName 注解 来增强符号运算符的定义。

最后,语法更改允许在多行表达式中将中缀运算符写在左侧。

infix 修饰符

方法定义中的 infix 修饰符允许将该方法用作中缀运算。示例

import scala.annotation.targetName

trait MultiSet[T]:

  infix def union(other: MultiSet[T]): MultiSet[T]

  def difference(other: MultiSet[T]): MultiSet[T]

  @targetName("intersection")
  def *(other: MultiSet[T]): MultiSet[T]

end MultiSet

val s1, s2: MultiSet[Int]

s1 union s2         // OK
s1 `union` s2       // also OK but unusual
s1.union(s2)        // also OK

s1.difference(s2)   // OK
s1 `difference` s2  // OK
s1 difference s2    // gives a deprecation warning

s1 * s2             // OK
s1 `*` s2           // also OK, but unusual
s1.*(s2)            // also OK, but unusual

涉及字母数字运算符的中缀运算已弃用,除非满足以下条件之一

  • 运算符定义带有 infix 修饰符,或
  • 运算符是用 Scala 2 编译的,或
  • 运算符后面跟着一个左大括号。

字母数字运算符是由字母、数字、$_ 字符或任何 Unicode 字符 c 组成的运算符,对于这些字符,java.lang.Character.isIdentifierPart(c) 返回 true

涉及符号运算符的中缀运算始终允许,因此对于具有符号名称的方法,infix 是多余的。

infix 修饰符也可以赋予类型

infix type or[X, Y]
val x: String or Int = ...

动机

infix 修饰符的目的是在整个代码库中实现方法或类型应用方式的一致性。其理念是,方法的作者决定该方法应该作为中缀运算符应用还是以常规方式应用。使用站点随后一致地实施该决定。

细节

  1. infix 是一个软修饰符。除了在修饰符位置之外,它被视为一个普通标识符。

  2. 如果一个方法覆盖了另一个方法,它们的 infix 注释必须一致。要么都用 infix 注释,要么都不注释。

  3. infix 修饰符可以赋予方法定义。infix 方法的第一个非接收器参数列表必须定义正好一个参数。示例

    infix def op1(x: S): R             // ok
    infix def op2[T](x: T)(y: S): R    // ok
    infix def op3[T](x: T, y: S): R    // error: two parameters
    
    extension (x: A)
      infix def op4(y: B): R          // ok
      infix def op5(y1: B, y2: B): R  // error: two parameters
    
  4. infix 修饰符也可以赋予具有正好两个类型参数的类型、特质或类定义。像

    infix type op[X, Y]
    

    这样的中缀类型可以使用中缀语法应用,即 A op B

  5. 为了顺利迁移到 Scala 3.0,字母数字运算符将从 Scala 3.1 开始弃用,或者如果在 Dotty/Scala 3 中指定了 -source future 选项。

@targetName 注释

建议符号运算符的定义包含一个 @targetName 注释,该注释提供一个使用字母数字名称对运算符进行编码的方式。这有几个好处

  • 它有助于 Scala 和其他语言之间的互操作性。可以使用目标名称从另一种语言调用 Scala 定义的符号运算符,这避免了必须记住符号名称的底层编码。
  • 它有助于堆栈跟踪和其他运行时诊断的可读性,其中将显示用户定义的字母数字名称,而不是底层编码。
  • 它通过提供一个作为符号运算符别名的替代常规名称来作为文档工具。这使得定义也更容易在搜索中找到。

语法更改

中缀运算符现在可以出现在多行表达式中的行首。示例

val str = "hello"
  ++ " world"
  ++ "!"

def condition =
  x > 0
  ||
  xs.exists(_ > 0)
  || xs.isEmpty

以前,这些表达式会被拒绝,因为编译器的分号推断会将延续 ++ " world"|| xs.isEmpty 视为单独的语句。

为了使此语法起作用,规则被修改为不在前导中缀运算符前面推断分号。前导中缀运算符

  • 一个符号标识符,例如 +approx_==,或一个在反引号中的标识符,它
  • 开始新行,并且
  • 不紧跟空白行,并且
  • 紧跟至少一个空格字符和一个可以开始表达式的标记。
  • 此外,如果运算符出现在它自己的行上,下一行必须至少具有与运算符相同的缩进宽度。

示例

freezing
  | boiling

这被识别为单个中缀操作。与以下内容进行比较

freezing
  !boiling

这被视为两个语句,freezing!boiling。区别在于,只有第一个示例中的运算符后面跟着一个空格。

另一个示例

println("hello")
  ???
  ??? match { case 0 => 1 }

此代码被识别为三个不同的语句。??? 在语法上是一个符号标识符,但它的任何出现都不紧跟空格和可以开始表达式的标记。

一元运算符

一元运算符不能具有显式参数列表,即使它们为空。一元运算符是一个名为“unary_op”的方法,其中 op+-!~ 之一。