ITmob-Ly
发布于 2023-11-02 / 115 阅读
0

Kotlin 中的运算符重载

Kotlin-Blog

在 Kotlin 中有很多运算符,其中很多都可以重载。有时使用直观的操作符比使用方法可读性更强。本文是Kotlin官方文档中 运算符重载 的翻译:


Kotlin 允许你对数据类型的一组预定义的操作符提供自定义的实现函数. 这些操作符有预定义的表达符号(比如 + 或 *), 以及预定义的优先级. 要实现这些操作符, 需要对相应的数据类型实现一个特定名称的 成员函数 或 扩展函数, 这里的数据类型, 对于二元操作符, 是指左侧操作数的类型, 对于一元操作符, 是指唯一一个操作数的类型.

要重载操作符, 要对相应的函数使用 operator 修饰符.

interface IndexedContainer {
    operator fun get(index: Int)
}

如果在后代类中 重载 操作符, 可以省略 operator:

class OrdersList: IndexedContainer {
    override fun get(index: Int) { /*...*/ }
}

一元操作符

一元前缀操作符

表达式翻译为
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()

上表告诉我们说, 当编译器处理一元操作符时, 比如表达式 +a, 它将执行以下步骤:

  • 确定 a 的类型, 假设为 T.
  • 查找带有 operator 修饰符, 无参数的 unaryPlus() 函数, 而且函数的接受者类型为 T, 也就是说, T 类型的成员函数或扩展函数.
  • 如果这个函数不存在, 或者找到多个, 则认为是编译错误.
  • 如果这个函数存在, 并且返回值类型为 R, 则表达式 +a 的类型为 R.

这些操作符, 以其其它所有操作符, 都对 基本类型 进行了优化 因此不会发生函数调用, 并由此产生性能损耗.

举例来说, 我们可以这样来重载负号操作符:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
   println(-point)  // 打印结果为 "Point(x=-10, y=-20)"
}

递增与递减操作符

表达式翻译为
a++a.inc() (参见下文)
a--a.dec() (参见下文)

inc() 和 dec() 函数必须返回一个值, 这个返回值将会赋值给使用 ++ 或 -- 操作符的对象变量. 这两个函数不应该改变调用 inc 或 dec 函数的对象的内容.

对于 后缀 形式操作符, 比如 a++, 编译器解析时将执行以下步骤:

  • 确定 a 的类型, 假设为 T.
  • 查找带有 operator 修饰符, 无参数的 inc() 函数, 而且函数的接受者类型为 T.
  • 检查函数的返回值类型是不是T 的子类型.

计算这个表达式所造成的影响是:

  • 将 a 的初始值保存到临时变量 a0 中.
  • 将 a0.inc() 的结果赋值给 a.
  • 返回 a0, 作为表达式的计算结果值.

对于 a--, 计算步骤完全类似.

对于 前缀 形式的操作符 ++a 和 --a, 解析过程是一样的, 计算表达式所造成的影响是:

  • 将 a.inc() 的结果赋值给 a.
  • 返回 a 的新值, 作为表达式的计算结果值.

二元操作符

算数操作符

表达式翻译为
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a..ba.rangeTo(b)

对于上表中的操作符, 编译器只是简单地解析 翻译为 列中的表达式.

下面是一个 Counter 类的例子, 它从一个给定的值开始计数, 可以通过 + 操作符递增:

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

in 操作符

表达式翻译为
a in bb.contains(a)
a !in b!b.contains(a)

对于 in 和 !in 操作符, 解析过程也是一样的, 但参数顺序被反转了.

下标访问操作符

表达式翻译为
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i_1, ..., i_n]a.get(i_1, ..., i_n)
a[i] = ba.set(i, b)
a[i, j] = ba.set(i, j, b)
a[i_1, ..., i_n] = ba.set(i_1, ..., i_n, b)

方括号被翻译为, 使用适当个数的参数, 对 get 和 set 函数的调用.

函数调用操作符

表达式翻译为
a()a.invoke()
a(i)a.invoke(i)
a(i, j)a.invoke(i, j)
a(i_1, ..., i_n)a.invoke(i_1, ..., i_n)

圆括号被翻译为, 使用适当个数的参数, 调用 invoke 函数.

计算并赋值

表达式翻译为
a += ba.plusAssign(b)
a -= ba.minusAssign(b)
a *= ba.timesAssign(b)
a /= ba.divAssign(b)
a %= ba.remAssign(b)

对于赋值操作符, 比如 a += b, 编译器执行以下步骤:

  • 如果上表中右列的函数可用:
    • 如果对应的二元操作函数(也就是说, 对于 plusAssign() 来说, plus() 函数) 也可用, a 是一个值可变的变量, plus 的返回类型是 a 的类型的子类型, 报告错误(歧义).
    • 确认函数的返回值类型为 Unit, 否则报告错误.
    • 生成 a.plusAssign(b) 的代码.
  • 否则, 尝试生成 a = a + b 的代码(这里包括类型检查: a + b 的类型必须是 a 的子类型).

在 Kotlin 中, 赋值操作 不是 表达式.

相等和不等比较操作符

表达式翻译为
a == ba?.equals(b) ?: (b === null)
a != b!(a?.equals(b) ?: (b === null))

这些操作符是通过 [equals(other: Any?): Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/equals.html) 函数来实现的, 可以重载这个函数, 来实现自定义的相等判断. 同名但不同参数的任何其他函数 (比如 equals(other: Foo)) 都不会被调用.

=== 和 !== (同一性检查) 操作符不允许重载, 因此对这两个操作符不存在约定.

== 操作符是特殊的: 它被翻译为一个复杂的表达式, 其中包括对 null 值的处理. null == null 的判断结果永远为 true, 对于非 null 的 xx == null 永远为 false, 并且不会调用 x.equals().

比较操作符

表达式翻译为
a > ba.compareTo(b) > 0
a < ba.compareTo(b) < 0
a >= ba.compareTo(b) >= 0
a <= ba.compareTo(b) <= 0

所有的比较操作符都被翻译为对 compareTo 函数的调用, 这个函数的返回值必须是 Int 类型.

属性委托操作符

关于 provideDelegategetValue 和 setValue 操作符函数, 请参见 委托属性.

对命名函数的中缀式调用

使用 infix 关键字标记的函数, 也可以使用中缀标记法(infix notation)来调用(调用时省略点号和括号). 中缀函数需要满足以下条件:

infix fun Int.shl(x: Int): Int { ... }

// 使用中缀标记法调用函数
1 shl 2

// 上面的语句等价于
1.shl(2)

中缀函数调用的优先级, 低于算数运算符, 类型转换, 以及 rangeTo 运算符. 以下表达式是等价的:

  • 1 shl 2 + 3 等价于 1 shl (2 + 3)
  • 0 until n * 2 等价于 0 until (n * 2)
  • xs union ys as Set<*> 等价于 xs union (ys as Set<*>)

另一方面, 中缀函数调用的优先级, 高于布尔值运算符 && 和 ||is 和 in 检查, 以及其他运算符. 以下表达式是等价的:

  • a && b xor c 等价于 a && (b xor c)
  • a xor b in c 等价于 (a xor b) in c

注意, 中缀函数的接受者和参数都需要明确指定. 如果使用中缀标记法调用当前接受者的一个方法, 需要明确指定 this. 这是为了保证语法解析不出现歧义.

class MyStringCollection {
    infix fun add(s: String) { /*...*/ }

    fun build() {
        this add "abc"   // 正确用法
        add("abc")       // 正确用法
        //add "abc"      // 错误用法: 方法的接受者必须明确指定
    }
}