Kotlin 运算符重载基础
Kotlin 允许通过定义特定函数来重载标准运算符,这使得自定义类型能够像内置类型一样参与运算,极大地提升了代码的可读性和表达力。这种机制被称为'运算符重载'(Operator Overloading)。在 Kotlin 中,所有用于重载运算符的函数都必须使用 operator 关键字进行修饰。如果不加此关键字,编译器会报错或将其视为普通函数。
为什么需要运算符重载?
运算符重载是构建领域特定语言(DSL)的关键技术之一。它允许开发者编写出更接近自然语言的代码,例如数学公式、金融计算或图形变换逻辑。通过重载,我们可以让自定义对象支持直观的语法糖,减少样板代码。
重载算术运算符
二元算术运算符
要重载二元运算符(如 +, -, *, /),需要在类中定义对应的成员函数或使用扩展函数。例如,要实现加法运算,需定义 plus 函数。
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
使用方式:val p = p1 + p2 等价于 p1.plus(p2)。
除了成员函数,也可以定义为扩展函数,这常用于为第三方库的类添加功能而不修改其源码。例如,给 String 添加一个特殊的拼接逻辑。
可重载的二元算术运算符列表:
| 运算符 | 对应函数 |
|---|
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
与 Java 的互操作性:
从 Java 调用 Kotlin 的运算符函数时,只需像普通函数一样调用即可。当 Kotlin 调用 Java 方法时,如果 Java 方法的名称和参数符合 Kotlin 的约定,也可以使用运算符语法。Java 没有 operator 关键字的概念,因此只要参数匹配即可。
注意:Kotlin 不会自动支持交换性(commutativity)。如果希望 a + b 和 b + a 都有效且类型不同,需要分别定义。例如,如果左操作数是 Int,右操作数是 Point,你需要定义一个接收 Int 作为 receiver 的扩展函数。
复合赋值运算符
Kotlin 支持 +=, -=, *=, /= 等复合赋值运算符。这些运算符通常映射到 plusAssign, minusAssign 等函数。
class MutableCounter(var count: Int) {
operator fun plusAssign(value: Int) {
count += value
}
}
可变性与不可变性:
设计类时应保持一致性。如果类是不可变的(Immutable),只提供返回新值的 plus;如果是可变的(Mutable),提供 plusAssign。同时提供两者可能导致编译器报错,因为 += 会优先尝试调用 plusAssign。如果一个类是不可变的,那么就应该只提供返回一个新值的运算。如果一个类是可变的,那么只需要提供 plusAssign 运算即可。
一元运算符
一元运算符包括 +a, -a, !a, ++a, --a 等。
| 运算符 | 对应函数 |
|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a / a++ | inc |
--a / a-- | dec |
编译器会自动处理前缀和后缀自增/自减的语义差异,无需分开定义。但要注意,inc 和 dec 必须返回 Unit 或者被忽略的值,具体取决于上下文。
重载比较运算符
等号运算符:equals
Kotlin 中的 == 运算符会被转换为 equals() 方法的调用,并包含空值检查。
a == b 等价于 a?.equals(b) ?: (b === null)。
对于数据类(data class),equals 由编译器自动生成,基于属性值进行比较。手动实现时需先检查引用同一性(===),再检查类型和属性值。
注意事项:
equals 不能实现为扩展函数,因为继承自 Any 类的实现始终优先于扩展函数。
=== 恒等运算符不能被重载,它始终检查内存地址是否相同。
排序运算符:compareTo
若类实现了 Comparable 接口,则可以使用 <, >, <=, >= 等比较运算符。compareTo 必须返回 Int 类型:负数表示小于,0 表示相等,正数表示大于。
推荐使用标准库的 compareValuesBy 函数简化实现,它可以按顺序比较多个属性:
override fun compareTo(other: Person): Int = compareValuesBy(this, other, Person::age, Person::name)
集合与区间的约定
下标访问:get 和 set
方括号 [] 是访问元素的便捷语法。读取调用 get,写入调用 set。
class Matrix(private val rows: Array<Array<Int>>) {
operator fun get(row: Int, col: Int): Int = rows[row][col]
operator fun set(row: Int, col: Int, value: Int) {
rows[row][col] = value
}
}
get 的参数可以是任意类型,不仅限于 Int。当你对 map 使用下标运算符时,参数类型是键的类型,它可以是任何类型,还可以定义具有多个参数的 get 方法,比如你要实现一个类来表示二维数组或者矩阵。
in 运算符
in 用于检查元素是否存在,底层调用 contains 函数。
element in collection 等价于 collection.contains(element)。
区间:rangeTo
使用 .. 创建区间,底层调用 rangeTo 函数。
start..end 等价于 start.rangeTo(end)。
如果类实现了 Comparable,可以直接使用标准库提供的 rangeTo 实现。
注意,表达式 0..n.forEach{print(it)} 不会被编译,因为必须把区间表达式括起来才能调用它的方法:(0..n).forEach{print(it)}。
迭代器:iterator
for 循环依赖 iterator 约定。
for (item in collection) 等价于 collection.iterator().forEach { ... }。
可以为自定义类型定义 iterator 扩展函数,使其支持遍历。
为自己的类定义 iterator 方法,使用 LocalDate 作为类型参数。如上一节所示,rangeTo 库函数返回一个 CloseRange 的实例,并且 ClosedRange< LocalDate>的 iterator 扩展允许在 for 循环中使用区间的实例。
最佳实践总结
- 可读性优先:仅在运算符含义明确时使用重载,避免滥用导致逻辑晦涩。例如,不要重载
+ 来做字符串拼接以外的奇怪操作。
- 一致性:保持类的可变性一致,避免混用
plus 和 plusAssign。如果一个类是不可变的,那么就应该只提供返回一个新值的运算。
- 文档化:重载运算符应清晰注释,说明其行为是否符合数学直觉。
- 空值安全:注意
equals 和比较运算符中的空值处理逻辑,确保不会抛出 NullPointerException。
- 性能考量:虽然重载方便,但过度使用可能影响性能,特别是在高频调用的场景下,需权衡可读性与执行效率。