运算符重载:让 Kotlin 运算符“听懂”你的类
运算符重载,说白了就是让 Kotlin 的 +、-、* 这些符号,能操作你自定义的类。我刚开始学 Kotlin 时,觉得这功能挺花哨的。直到有一次写一个矩阵运算库,发现用 matrix1 + matrix2 比 matrix1.add(matrix2) 优雅太多了。嗯,从那以后我就爱上了这个特性。
一、operator 关键字:重载的“开关”
在 Kotlin 里,想重载运算符,必须用 operator 关键字修饰函数。这是语法硬性要求,少写了编译器会报错。
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
fun main() {
val p1 = Point(1, 2)
val p2 = Point(3, 4)
val p3 = p1 + p2 // 等价于 p1.plus(p2)
println(p3) // Point(x=4, y=6)
}
我个人习惯把运算符重载函数定义在类内部,这样代码更内聚。但如果类不是你写的(比如第三方库),可以用扩展函数加 operator:
operator fun Point.times(scale: Int): Point {
return Point(x * scale, y * scale)
}
fun main() {
val p = Point(2, 3)
val scaled = p * 5 // Point(x=10, y=15)
}
plus,乘法用 times。Kotlin 官方文档有完整映射表,建议收藏。
二、一元运算符:单兵作战
一元运算符只操作一个对象。常见的有 +a、-a、!a、++a、a--。它们对应的函数名分别是 unaryPlus、unaryMinus、not、inc、dec。
data class Vector(val x: Int, val y: Int) {
operator fun unaryMinus() = Vector(-x, -y)
operator fun inc() = Vector(x + 1, y + 1)
}
fun main() {
val v = Vector(3, 4)
val neg = -v // Vector(-3, -4)
var v2 = Vector(1, 2)
v2++ // Vector(2, 3)
}
这里有个坑:inc 和 dec 必须返回一个新对象,不能修改自身。因为 a++ 在 Kotlin 里是先返回旧值再自增,底层会调用 inc() 并重新赋值。我曾经在项目里踩过这个坑,直接修改了 this 的字段,结果并发环境下数据全乱了。
!a 返回 Boolean 完全合理。但 inc/dec 建议返回相同类型,否则 ++ 语义会变得很奇怪。
三、二元运算符:双剑合璧
二元运算符是重载的重头戏。除了常见的 plus、minus、times、div、rem(取余),还有 rangeTo(..)、contains(in)等。
| 运算符 | 函数名 | 示例 |
|---|---|---|
| + | plus | a + b |
| - | minus | a - b |
| * | times | a * b |
| / | div | a / b |
| % | rem | a % b |
| .. | rangeTo | a..b |
| in | contains | a in b |
data class Money(val amount: Long, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "货币类型不一致" }
return Money(amount + other.amount, currency)
}
operator fun times(rate: Double): Money {
return Money((amount * rate).toLong(), currency)
}
}
fun main() {
val salary = Money(5000, "CNY")
val bonus = Money(2000, "CNY")
val total = salary + bonus // Money(7000, "CNY")
val tax = salary * 0.1 // Money(500, "CNY")
}
你想想看,如果不用运算符重载,上面这段代码得写成 salary.plus(bonus) 和 salary.times(0.1),可读性差了一大截。
四、比较运算符:equals 和 compareTo
Kotlin 的 == 和 != 对应 equals 函数。注意 ===(引用比较)不能重载。而 <、>、<=、>= 对应 compareTo 函数。
data class Person(val name: String, val age: Int) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return this.age - other.age
}
}
fun main() {
val p1 = Person("Alice", 25)
val p2 = Person("Bob", 30)
println(p1 < p2) // true,因为 25 < 30
println(p1 == Person("Alice", 25)) // true,data class 自动生成 equals
}
这里有个细节:equals 重载时,参数类型必须是 Any?,不能是具体类型。否则 Kotlin 不会把它当作 == 的对应函数。我见过有人写成 equals(other: Person),结果 == 永远走的是默认的引用比较,调试了半天才发现。
Comparable 接口后,compareTo 自动支持所有比较运算符。这是 Kotlin 的约定,不需要额外加 operator 关键字。
五、区间运算符:.. 和 in
.. 运算符对应 rangeTo 函数,返回一个 ClosedRange。而 in 和 !in 对应 contains 函数。这两个组合起来,能写出非常自然的区间判断代码。
data class Date(val year: Int, val month: Int, val day: Int) : Comparable<Date> {
override fun compareTo(other: Date): Int {
return compareValuesBy(this, other, { it.year }, { it.month }, { it.day })
}
operator fun rangeTo(other: Date): ClosedRange<Date> {
return object : ClosedRange<Date> {
override val start: Date get() = this@Date
override val endInclusive: Date get() = other
}
}
}
fun main() {
val start = Date(2024, 1, 1)
val end = Date(2024, 12, 31)
val holiday = Date(2024, 10, 1)
if (holiday in start..end) {
println("假期在2024年内")
}
}
我个人觉得区间运算符最妙的地方在于,它让代码读起来像自然语言。你想想看,holiday in start..end 这句话,不懂编程的人也能猜出大概意思。
Comparable,rangeTo 可以直接用 start..end 语法,Kotlin 标准库会帮你生成默认的 ClosedRange。但如果你想自定义区间行为(比如半开区间),就需要像上面那样手动实现。
六、知识体系总览
下面这张图总结了运算符重载的核心分类和对应关系,方便你快速查阅。
七、避坑指南与最佳实践
运算符重载虽然好用,但用不好就是灾难。我总结了几条经验:
- 不要滥用:只有运算符的语义和你的类操作天然匹配时才重载。比如用
+表示集合合并没问题,但用+表示“发送邮件”就太抽象了。 - 保持对称性:如果定义了
a + b,最好也考虑b + a是否合理。比如Money * 2和2 * Money都应该支持。 - 注意不可变性:运算符重载函数应该返回新对象,不要修改操作数。这是函数式编程的惯例,也是 Kotlin 标准库的做法。
- 文档要写清楚:团队协作时,重载运算符一定要写文档注释。否则同事看到
user + role可能会一脸懵逼。
Order 类的 plus 运算符用来合并订单。结果新同事接手后,以为 + 是金额相加,直接写了个 order1 + order2 去算总价,导致线上数据异常。从那以后,我规定团队里运算符重载必须经过 code review,并且要在函数上方加 @Documented 注解。
好了,运算符重载的核心内容就这些。记住一个原则:让代码读起来像自然语言,而不是让运算符变成炫技的工具。你想想看,好的代码应该是“自解释”的,运算符重载只是帮你达到这个目的的手段之一。