1. 概述

本文将带你了解一些基础的 Scala 运算符,并深入探讨运算符的优先级和结合性规则。

2. 方法与运算符

在 Scala 中,所有的运算符本质上都是方法。运算符只是调用方法的一种语法糖或简写方式。

比如我们常用的加法运算符 +

assert(1 + 2 == 3)

这里我们使用 + 符号作为运算符来对两个数字进行相加操作。实际上,Scala 在内部是调用了名为 + 的方法。我们也可以使用点号(dot)显式地调用该方法,结果是一样的:

assert(1.+(2) == 3)

不仅如此,Scala 中任何接受至少一个参数的方法都可以当作运算符来使用。例如 String.charAt() 方法,用于获取字符串中指定索引位置的字符,通常我们会这样调用:

assert("Baeldung".charAt(0) == 'B')

也可以像下面这样以运算符的形式调用:

val char = "Baeldung" charAt 0
assert(char == 'B')

上面的加法运算符 +String.charAt() 都属于中缀运算符(infix operators)。我们可以使用中缀表示法来调用任何至少接受一个参数的方法。

如果方法接受多个参数,则必须将所有参数放在一对括号中才能使用中缀语法。例如,String.replace() 方法接受两个参数:

assert("Baeldung".replace('g', 'G') == "BaeldunG")

我们把所有的 g 替换成了大写的 G。通过添加括号,它也可以作为中缀运算符来调用:

val str = "Baeldung" replace ('g', 'G')
assert(str == "BaeldunG")

2.1. 前缀与后缀运算符表示法

到目前为止我们看到的都是中缀运算符的例子,即运算符位于两个操作数之间。除此之外,还有两种类型的运算符表示法:前缀(prefix)和后缀(postfix)。

前缀运算符出现在其操作数之前。例如 -10 中的 -。所有的前缀运算符都会被转换为名称为 unary_<operator> 的方法:

assert(10.unary_- == -10)

后缀运算符则出现在其操作数之后。例如我们可以将 String.toUpperCase 方法作为后缀运算符来使用:

val strUpperCase = "baeldung" toUpperCase
assert(strUpperCase == "BAELDUNG")

由于前缀和后缀运算符都只接受一个操作数,因此它们也被称为一元运算符(unary operators)。

3. 基础运算符

Scala 继承了 Java 的基本运算符,并且行为基本一致,但有一些细微差别。下面来看一下 Scala 支持的基本运算符类型:

3.1. 算术运算符

Scala 提供了以下几种二元算术运算符,适用于数值类型:

✅ 加法(+)
✅ 减法(-)
✅ 乘法(*)
✅ 除法(/)
✅ 取模(%)

这些运算符都可以使用中缀语法调用:

assert(1 + 2 == 3)
assert(3.1 - 1.0 == 2.1)
assert(2 * 6 == 12)
assert(15 / 6 == 2)
assert(15 % 6 == 3)

此外,Scala 还支持两个一元运算符用来表示数字的正负性:

✅ 正号(+),对应方法为 unary_+
✅ 负号(–),对应方法为 unary_-

其中,+ 表示数字是正数,而 - 表示负数:

val num = 10
assert(-num == -10)
assert(10 + -num == 0)

如果没有显式指定符号,默认情况下数字字面量是正数。

3.2. 关系运算符

Scala 中有以下五个关系运算符:

✅ 大于(>)
✅ 小于(<)
✅ 大于等于(>=)
✅ 小于等于(<=)

所有这些关系运算符的结果都是布尔值(Boolean):

assert(10 < 20 == true) 
assert(10 > 20 == false) 
assert(3.0 >= 2.5 == true) 
assert(3.0 <= 2.5 == false)

此外,还有一个一元逻辑非运算符(!)用于取反布尔值:

assert(!true == false)

3.3. 逻辑运算符

逻辑运算符包括逻辑或(|||)和逻辑与(&&&)。它们接受布尔类型的操作数,并返回布尔值。只有当两个操作数至少有一个为真时,逻辑或才返回真;只有当两个操作数都为真时,逻辑与才返回真。

assert(true || false == true)
assert(true && false == false)

⚠️ &&|| 是短路运算符(short-circuit operators),这意味着表达式不一定总是会计算两边的操作数。如果左侧已经能确定整个表达式的结果,右侧就不会再执行。

举个例子,如果 && 左侧是 false,那么无论右侧是什么结果,整个表达式的值都一定是 false,所以右侧不会被执行。同理,如果 || 左侧是 true,那么整个表达式就是 true,右侧也不会执行。

来看一个具体例子:

def printTrue() : Boolean = {
  println("true");
  true
}

def printFalse() : Boolean = {
  println("false"); 
  false
}
val result1 = printFalse() && printTrue() // 只输出 "false"
assert(result1 == false)
val result2 = printTrue() && printFalse() // 输出 "true" 和 "false"
assert(result2 == false)

在第一个例子中,因为 printFalse() 返回 false,所以不需要执行右边的 printTrue(),直接就能判断出整个表达式为 false

而在第二个例子中,因为 printTrue() 返回 true,所以还需要进一步判断右边的值,因此两边都被执行了。

如果你希望两边都执行,可以使用 &|

val result3 = printFalse() & printTrue()  // 输出 "false" 和 "true"
assert(result3 == false)

可以看到,& 不具备短路特性,两边都会执行。

3.4. 位运算符

位运算符对整数类型的每个二进制位进行操作。Scala 支持以下几种位运算符:

✅ 按位或(|)
✅ 按位与(&)
✅ 按位异或(^)
✅ 按位取反(~)

它们分别对每一位进行相应的运算:

val bitwiseAndResult = 2 & 6
assert(bitwiseAndResult == 2)

在这个例子中,2(0010)和 6(0110)按位与后得到 2(0010)。同样地,|^ 分别执行按位或和按位异或。

~ 是按位取反运算符,它会把每一位都反转:

assert(~2 == -3)

Scala 还提供了三种位移运算方法:

✅ 左移(<<)
✅ 有符号右移(>>)
✅ 无符号右移(>>>)

所有这些位移运算符都会根据右操作数的值将左操作数的整数值向左或向右移动若干位。

✅ 左移运算符会将位向左移动,并在右侧补零:

assert(2 << 2 == 8)

在这个例子中,2(0010)左移两位变成 8(1000)。

✅ 有符号右移会在高位补上原数的符号位(即最高位):

assert(-8 >> 2 == -2)

-8 的二进制形式是 11111111111111111111111111111000。右移两位后,高位补的是符号位 1,结果为 -2(11111111111111111111111111111110)。

✅ 无符号右移则始终用 0 补高位:

assert(-8 >>> 2 == 1073741822)

无符号右移将 -8(11111111111111111111111111111000)右移两位,高位补 0,得到 1073741822(00111111111111111111111111111110)。

3.5. 相等性运算符

在 Scala 中,我们使用 == 来判断两个对象是否相等,使用 != 判断不相等。

⚠️ 与 Java 不同的是,在 Scala 中你可以对任何对象使用 == 来进行相等性判断,而不仅仅局限于基本类型。

4. 运算符优先级

当一个表达式中含有多个运算符时,它们的执行顺序取决于运算符的优先级。

例如,表达式 2 + 3 * 6 的结果是 20 而不是 30,这是因为乘法运算符 * 的优先级高于加法运算符 +,所以会先执行乘法。

由于 Scala 的运算符本质上是方法,因此它们的优先级是由方法名的第一个字符决定的:

(所有其他特殊字符)
* / %
+ -
:
=!
< >
&
^
|
(所有字母)

表格中越靠上的字符优先级越高。如果第一个字符相同,则从左到右依次计算:

assert(2 + 3 * 4 == 14)

这个表达式会被解释为 2 + (3 * 4),因为 * 的优先级高于 +,最终结果是 14。

assert(4 - 2 + 1 == 3)

因为 +- 具有相同的优先级,所以从左到右计算,即 (4 - 2) + 1

⚠️ 有一个例外:如果运算符以等号(=)结尾,且不是比较运算符(如 <=, >=, ==, !=),那么它的优先级就等同于赋值运算符 =

var num = 10
num += 2 * 10
assert(num == 30)

这里表达式被解释为 num += (2 * 10),因为 += 的优先级与 = 相同,低于 *

5. 运算符结合性

当多个具有相同优先级的运算符出现在同一个表达式中时,结合性决定了这些运算符的分组方式。结合性由方法名的最后一个字符决定。

✅ 如果方法名以冒号(:)结尾,则该方法会将左侧的操作数传递给右侧的方法调用;
✅ 如果方法名以其他字符结尾,则会将右侧的操作数传递给左侧的方法调用。

例如:

assert(2 * 3 == 6)

这里实际上是 2.*(3)

再来看 List::: 方法:

assert(List(1,2) ::: List(3) == List(1, 2, 3))

结合性在这里发生了变化,它实际上是这样执行的:

assert(List(3).:::(List(1,2)) == List(1, 2, 3))

6. 总结

本文介绍了 Scala 中的所有运算符其实都是方法,我们学习了各种基本运算符及其优先级和结合性规则。

一如既往,本文中的所有代码示例都可以在 GitHub 上找到。


原始标题:Introduction to Scala Operators