Kotlin 运算符重载详解:为什么 a += b 有时改对象,有时换对象?

简介

同样一行 Kotlin 代码:

a += b

有时会直接修改 a 指向的对象,有时却会先计算 a + b,再把新对象赋值给 a

再看几种熟悉的写法:

val total = price + freight
val allowed = userId in permissionSet
val cell = board[2, 3]
val result = rule(order)
val versions = startVersion..endVersion

这些符号看起来像 Kotlin 内置语法,背后其实都可能是普通函数调用:

price.plus(freight)
permissionSet.contains(userId)
board.get(2, 3)
rule.invoke(order)
startVersion.rangeTo(endVersion)

这就是 Kotlin Operator Overloading,运算符重载。

它并不是让符号可以随意发挥,而是建立了一套固定约定:

固定运算符
固定函数名
固定参数形式

例如 + 只能映射到 plus,不能改成 add[] 只能映射到 getsetin 只能映射到 contains

运算符重载用得合适,金额、向量、矩阵、区间和 DSL 会非常自然。用得随意,一个 + 号也可能藏住删除数据、网络请求甚至数据库写入。

第一个运算符:让坐标支持加法

先定义一个二维坐标:

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

普通写法:

fun Point.add(other: Point): Point {
    return Point(
        x = x + other.x,
        y = y + other.y
    )
}

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(3, 5)

    println(p1.add(p2))
}

输出:

Point(x=13, y=25)

把函数名改成 Kotlin 约定的 plus,再加上 operator

data class Point(
    val x: Int,
    val y: Int
) {
    operator fun plus(other: Point): Point {
        return Point(
            x = x + other.x,
            y = y + other.y
        )
    }
}

调用时就能使用 +

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(3, 5)

    val result = p1 + p2
    println(result)
}

输出:

Point(x=13, y=25)

编译器按照约定解析:

p1 + p2

等价于:

p1.plus(p2)

去掉 operator 之后,p1.plus(p2) 仍然可以调用,但 p1 + p2 会编译失败。

运算符重载不是创建新符号

Kotlin 只允许重载语言规定好的运算符,不能发明一个新符号,也不能修改符号的优先级和结合方向。

例如:

val result = a + b * c

仍然会先计算 b * c,再计算 a + ...

它近似展开成:

val result = a.plus(b.times(c))

即使 plustimes 都是自定义函数,也不能改变 *+ 优先的规则。

运算符函数可以定义成:

  • 类的成员函数
  • 扩展函数

成员函数示例:

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

扩展函数示例:

operator fun Int.times(point: Point): Point {
    return Point(
        x = this * point.x,
        y = this * point.y
    )
}

fun main() {
    println(3 * Point(2, 4))
}

输出:

Point(x=6, y=12)

需要注意,成员函数的优先级高于扩展函数。扩展函数无法覆盖类里已经存在的同签名成员函数。

常用运算符映射表

一元运算符
表达式对应函数
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a++++aa.inc()
a----aa.dec()
二元算术运算符
表达式对应函数
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
区间和成员判断
表达式对应函数
a..ba.rangeTo(b)
a..<ba.rangeUntil(b)
a in bb.contains(a)
a !in b!b.contains(a)
索引和调用
表达式对应函数
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i] = valuea.set(i, value)
a[i, j] = valuea.set(i, j, value)
a()a.invoke()
a(x, y)a.invoke(x, y)
比较和相等
表达式对应逻辑
a < ba.compareTo(b) < 0
a > ba.compareTo(b) > 0
a <= ba.compareTo(b) <= 0
a >= ba.compareTo(b) >= 0
a == b空安全调用 a?.equals(b)
a != ba == b 的结果取反
复合赋值
表达式原地修改函数创建新值的后备写法
a += ba.plusAssign(b)a = a.plus(b)
a -= ba.minusAssign(b)a = a.minus(b)
a *= ba.timesAssign(b)a = a.times(b)
a /= ba.divAssign(b)a = a.div(b)
a %= ba.remAssign(b)a = a.rem(b)

后面的章节会把最容易出错的几类单独拆开。

实战 Demo:金额为什么适合重载运算符

金额具有明确的加法、减法、倍数和比较语义,很适合做成不可变值对象。

为了避免 Double 精度问题,示例使用最小货币单位保存金额:人民币使用“分”。

import java.math.BigDecimal

data class Money private constructor(
    private val minor: Long,
    val currency: String
) : Comparable<Money> {

    companion object {
        fun cny(yuan: Long, fen: Int = 0): Money {
            require(fen in 0..99) { "分必须在 0 到 99 之间" }
            return Money(yuan * 100 + fen, "CNY")
        }

        fun usd(dollar: Long, cent: Int = 0): Money {
            require(cent in 0..99) { "美分必须在 0 到 99 之间" }
            return Money(dollar * 100 + cent, "USD")
        }
    }

    operator fun plus(other: Money): Money {
        requireSameCurrency(other)
        return copy(minor = minor + other.minor)
    }

    operator fun minus(other: Money): Money {
        requireSameCurrency(other)
        return copy(minor = minor - other.minor)
    }

    operator fun times(quantity: Int): Money {
        require(quantity >= 0) { "数量不能小于 0" }
        return copy(minor = minor * quantity)
    }

    operator fun unaryMinus(): Money {
        return copy(minor = -minor)
    }

    override fun compareTo(other: Money): Int {
        requireSameCurrency(other)
        return minor.compareTo(other.minor)
    }

    private fun requireSameCurrency(other: Money) {
        require(currency == other.currency) {
            "不同币种不能直接计算:$currency${other.currency}"
        }
    }

    override fun toString(): String {
        val amount = BigDecimal.valueOf(minor, 2)
        return "$currency $amount"
    }
}

再提供一个扩展函数,让数量可以写在左边:

operator fun Int.times(price: Money): Money {
    return price * this
}

使用:

fun main() {
    val unitPrice = Money.cny(yuan = 19, fen = 90)
    val freight = Money.cny(yuan = 6)

    val subtotal = unitPrice * 3
    val total = subtotal + freight

    println(subtotal)
    println(total)
    println(total > Money.cny(50))
    println(2 * unitPrice)
}

输出:

CNY 59.70
CNY 65.70
true
CNY 39.80

代码读起来接近业务公式:

val total = unitPrice * 3 + freight

运算符函数仍然负责守住业务规则。不同币种相加会直接失败:

val cny = Money.cny(10)
val usd = Money.usd(10)

// 抛出 IllegalArgumentException
println(cny + usd)

这里的 + 符合直觉:相同币种相加得到新金额,原对象不变。

运算符不要求左右两边类型相同

plustimes 等函数的参数类型和返回类型可以按业务需要设计。

例如:

data class Basket(
    val items: List<String>
) {
    operator fun plus(product: String): Basket {
        return copy(items = items + product)
    }
}

fun main() {
    val basket = Basket(emptyList())
    val result = basket + "Keyboard" + "Mouse"

    println(result.items)
}

输出:

[Keyboard, Mouse]

这里是:

Basket + String -> Basket

但是交换顺序不会自动成立:

// 没有 String.plus(Basket),所以不能编译
// "Keyboard" + basket

运算符重载不会自动获得交换律。需要反方向语法时,必须再提供对应的成员函数或扩展函数。

一元运算符:负号、正号和逻辑取反

一元运算符没有额外参数。

data class Vector(
    val x: Int,
    val y: Int
) {
    operator fun unaryMinus(): Vector {
        return Vector(-x, -y)
    }

    operator fun unaryPlus(): Vector {
        return this
    }
}

fun main() {
    val vector = Vector(3, -5)

    println(-vector)
    println(+vector)
}

输出:

Vector(x=-3, y=5)
Vector(x=3, y=-5)

! 对应 not(),返回类型不强制必须是 Boolean,但实际设计最好符合“取反”直觉。

data class FeatureFlag(
    val enabled: Boolean
) {
    operator fun not(): FeatureFlag {
        return copy(enabled = !enabled)
    }
}

fun main() {
    val flag = FeatureFlag(enabled = true)
    println(!flag)
}

输出:

FeatureFlag(enabled=false)

++-- 不是普通的原地修改

++ 对应 inc()-- 对应 dec()

data class VersionCode(val value: Int) {
    operator fun inc(): VersionCode {
        return VersionCode(value + 1)
    }

    operator fun dec(): VersionCode {
        return VersionCode(value - 1)
    }
}

使用时变量必须能够重新赋值:

fun main() {
    var version = VersionCode(10)

    val old = version++

    println(old)
    println(version)
}

输出:

VersionCode(value=10)
VersionCode(value=11)

后置自增可以近似理解为:

val old = version
version = version.inc()

前置自增:

val current = ++version

可以近似理解为:

version = version.inc()
val current = version

inc()dec() 应返回新值,不应偷偷修改原对象。最终变量更新由编译器完成。

因此下面这种写法不能工作:

val version = VersionCode(10)

// 编译失败:val 不能重新赋值
// version++

比较运算符只需要一个 compareTo

<><=>= 都映射到 compareTo

实现版本号比较:

data class SemanticVersion(
    val major: Int,
    val minor: Int,
    val patch: Int
) : Comparable<SemanticVersion> {

    override fun compareTo(other: SemanticVersion): Int {
        return compareValuesBy(
            this,
            other,
            SemanticVersion::major,
            SemanticVersion::minor,
            SemanticVersion::patch
        )
    }

    override fun toString(): String {
        return "$major.$minor.$patch"
    }
}

使用:

fun main() {
    val current = SemanticVersion(2, 3, 10)
    val required = SemanticVersion(2, 2, 20)

    println(current > required)
    println(current >= SemanticVersion(2, 3, 10))
}

输出:

true
true

compareTo 返回值只看正负号:

小于 0:左边小于右边
等于 0:两边排序位置相同
大于 0:左边大于右边

不要假设它只能返回 -101

也不要直接用减法实现所有比较:

// 不推荐,极端数值可能溢出
return age - other.age

更稳妥的写法:

return age.compareTo(other.age)

compareTo 和 equals 最好保持一致

如果 a.compareTo(b) == 0,通常也应该让 a == b 成立。

否则把对象放进排序集合时容易出现意外结果。

例如 TreeSet 主要依赖排序结果判断元素位置。如果两个对象 compareTo 返回 0,但 equals 返回 false,集合行为会很难理解。

值对象可以优先使用 data class,让 equalshashCode 和属性值保持一致,再让 compareTo 使用同一组关键属性。

===== 根本不是一回事

Kotlin 的 == 检查结构相等,底层使用 equals

data class User(
    val id: Long,
    val name: String
)

fun main() {
    val a = User(1, "Tom")
    val b = User(1, "Tom")

    println(a == b)
    println(a === b)
}

输出:

true
false

原因:

a == b   属性值相等
a === b  是否为同一个对象引用

data class 自动生成 equals,所以两个不同对象也能通过 == 判断为相等。

普通类需要重写 equalshashCode

class Account(
    val number: String
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Account) return false
        return number == other.number
    }

    override fun hashCode(): Int {
        return number.hashCode()
    }
}

equals 是一个特殊约定。Any.equals 本身已经具有运算符能力,重写时使用 override 即可,不需要额外写 operator

override fun equals(other: Any?): Boolean

== 还是空安全的。表达式:

a == b

可以近似理解为:

a?.equals(b) ?: (b === null)

===!== 是引用比较,不能重载。

下标访问:让二维棋盘像数组一样使用

getset 可以接收多个索引参数,很适合矩阵、棋盘、表格和坐标容器。

class Board(
    private val rows: Int,
    private val columns: Int
) {
    private val cells = Array(rows) {
        Array(columns) { "." }
    }

    operator fun get(row: Int, column: Int): String {
        checkPosition(row, column)
        return cells[row][column]
    }

    operator fun set(row: Int, column: Int, value: String) {
        checkPosition(row, column)
        cells[row][column] = value
    }

    private fun checkPosition(row: Int, column: Int) {
        require(row in 0 until rows) { "行下标越界:$row" }
        require(column in 0 until columns) { "列下标越界:$column" }
    }

    override fun toString(): String {
        return cells.joinToString("\n") { row ->
            row.joinToString(" ")
        }
    }
}

使用:

fun main() {
    val board = Board(rows = 3, columns = 3)

    board[0, 0] = "X"
    board[1, 1] = "O"
    board[2, 2] = "X"

    println(board[1, 1])
    println(board)
}

输出:

O
X . .
. O .
. . X

编译器转换规则:

board[1, 1]

对应:

board.get(1, 1)

而:

board[1, 1] = "O"

对应:

board.set(1, 1, "O")

set 的最后一个参数是赋进去的值,前面的参数都是索引。

in 的调用方向是反的

看到:

userId in permissionSet

很容易误以为会调用:

userId.contains(permissionSet)

实际方向正好相反:

permissionSet.contains(userId)

定义一个权限集合:

class PermissionSet(
    private val allowedUserIds: Set<Long>
) {
    operator fun contains(userId: Long): Boolean {
        return userId in allowedUserIds
    }
}

fun main() {
    val permissions = PermissionSet(
        allowedUserIds = setOf(1001L, 1002L)
    )

    println(1001L in permissions)
    println(2001L !in permissions)
}

输出:

true
true

!in 不需要单独实现函数,它就是对 contains 的结果取反。

自定义区间:版本号也能使用 ..

前面已经让 SemanticVersion 实现了 Comparable

再增加 rangeTo

operator fun SemanticVersion.rangeTo(
    end: SemanticVersion
): ClosedRange<SemanticVersion> {
    return object : ClosedRange<SemanticVersion> {
        override val start: SemanticVersion = this@rangeTo
        override val endInclusive: SemanticVersion = end
    }
}

使用:

fun main() {
    val supported = SemanticVersion(2, 1, 0)..SemanticVersion(2, 5, 0)

    println(SemanticVersion(2, 3, 1) in supported)
    println(SemanticVersion(3, 0, 0) in supported)
}

输出:

true
false

这里组合了三层约定:

..          调用 rangeTo
in          调用 contains
范围比较    最终依赖 compareTo

区间只表示“从哪里到哪里”时,ClosedRange 已经够用。

如果还希望写:

for (version in supported) {
    // ...
}

那就需要额外定义如何产生下一个版本,并让返回对象支持迭代。一个区间能做成员判断,不代表它天然能够遍历。

invoke:对象为什么能像函数一样调用

invoke 对应圆括号调用:

object(argument)

例如订单折扣规则:

data class Order(
    val userLevel: Int,
    val total: Money
)

class DiscountRule(
    private val minimumLevel: Int,
    private val discount: Money
) {
    operator fun invoke(order: Order): Money {
        return if (order.userLevel >= minimumLevel) {
            order.total - discount
        } else {
            order.total
        }
    }
}

使用:

fun main() {
    val vipDiscount = DiscountRule(
        minimumLevel = 3,
        discount = Money.cny(10)
    )

    val order = Order(
        userLevel = 4,
        total = Money.cny(99)
    )

    val payable = vipDiscount(order)
    println(payable)
}

输出:

CNY 89.00

这行:

vipDiscount(order)

等价于:

vipDiscount.invoke(order)

invoke 适合表达“这个对象本身就是一条可执行规则”:

  • 校验规则
  • 价格规则
  • 路由匹配器
  • 数据转换器
  • DSL 节点

如果对象主要职责不是执行,普通命名函数通常更清楚。例如 repository(user) 很难看出是在查询、保存还是删除,写成 repository.find(user) 会更明确。

+= 为什么有时改对象,有时换对象

这是运算符重载最容易踩坑的地方。

只有 plusAssign:直接修改对象
class ShoppingCart {
    private val products = mutableListOf<String>()

    operator fun plusAssign(product: String) {
        products += product
    }

    override fun toString(): String {
        return products.toString()
    }
}

fun main() {
    val cart = ShoppingCart()

    cart += "Keyboard"
    cart += "Mouse"

    println(cart)
}

输出:

[Keyboard, Mouse]

这里调用:

cart.plusAssign("Keyboard")

cartval 也能使用 +=,因为变量没有重新赋值,只是对象内部状态发生了变化。

只有 plus:计算新对象后重新赋值
data class ImmutableCart(
    val products: List<String>
) {
    operator fun plus(product: String): ImmutableCart {
        return copy(products = products + product)
    }
}

fun main() {
    var cart = ImmutableCart(emptyList())

    cart += "Keyboard"
    cart += "Mouse"

    println(cart.products)
}

这时 cart += product 近似展开为:

cart = cart.plus(product)

因此变量必须是 var

如果写成 val

val cart = ImmutableCart(emptyList())

// 编译失败:需要重新给 cart 赋值
// cart += "Keyboard"
plus 和 plusAssign 不要同时定义成同样适用

如果同一个类型同时提供可用的 plusplusAssign,表达式 a += b 可能产生重载解析歧义,编译器不会稳定地替业务代码猜测“修改原对象”还是“创建新对象”。

更稳妥的设计:

不可变类型:提供 plus,返回新对象
可变类型:按需提供 plusAssign,修改当前对象
同一组参数不要让 plus 和 plusAssign 同时竞争

这也是 Kotlin 集合里 ListMutableList 行为容易让人困惑的根源之一。看到 += 时,必须结合变量类型、val/var 和可用运算符函数判断真实行为。

解构声明背后也是 operator 约定

下面的写法叫解构声明:

val (name, age) = user

它会调用:

val name = user.component1()
val age = user.component2()

data class 会自动生成 componentN

data class User(
    val name: String,
    val age: Int
)

fun main() {
    val user = User("Tom", 20)
    val (name, age) = user

    println(name)
    println(age)
}

普通类也能手动支持解构:

class Coordinate(
    private val longitude: Double,
    private val latitude: Double
) {
    operator fun component1(): Double = longitude
    operator fun component2(): Double = latitude
}

fun main() {
    val coordinate = Coordinate(120.15, 30.28)
    val (longitude, latitude) = coordinate

    println("$longitude, $latitude")
}

component1component2 没有对应的可见符号,但仍属于 Kotlin 的运算符约定体系。

返回顺序必须稳定且符合对象含义。解构位置没有字段名,一旦顺序设计混乱,调用处很难发现错误。

for 循环也依赖约定函数

for 循环并不要求对象必须实现 Java 的 Iterable 接口,只要能提供符合约定的迭代器即可。

class Countdown(
    private val start: Int
) {
    operator fun iterator(): Iterator<Int> {
        return object : Iterator<Int> {
            private var current = start

            override fun hasNext(): Boolean {
                return current >= 0
            }

            override fun next(): Int {
                return current--
            }
        }
    }
}

fun main() {
    for (number in Countdown(3)) {
        println(number)
    }
}

输出:

3
2
1
0

循环会使用 iterator(),再反复调用 hasNext()next()

实现标准 IteratorIterable 通常更清楚,也更容易和集合 API 配合。约定函数适合轻量 DSL 或无法修改原类型的场景。

属性委托也使用 operator 函数

属性委托语法:

val token by delegate

读取属性时会调用委托对象的 getValue

import kotlin.reflect.KProperty

class EnvironmentValue(
    private val values: Map<String, String>,
    private val key: String
) {
    operator fun getValue(
        thisRef: Any?,
        property: KProperty<*>
    ): String {
        return values[key]
            ?: error("缺少配置:$key,对应属性:${property.name}")
    }
}

class AppConfig(environment: Map<String, String>) {
    val apiUrl: String by EnvironmentValue(environment, "API_URL")
    val appName: String by EnvironmentValue(environment, "APP_NAME")
}

使用:

fun main() {
    val config = AppConfig(
        mapOf(
            "API_URL" to "https://example.com",
            "APP_NAME" to "Order Center"
        )
    )

    println(config.apiUrl)
    println(config.appName)
}

输出:

https://example.com
Order Center

可变委托属性还需要实现 setValue

operator fun setValue(
    thisRef: Any?,
    property: KProperty<*>,
    value: String
)

getValuesetValueprovideDelegate 没有 +[] 这样的符号,但同样通过 operator 约定驱动语言语法。

Demo:用 unaryPlus 做一个小型文本 DSL

Kotlin DSL 里经常看到这种写法:

+"订单创建成功"

它不是特殊字符串语法,而是 unaryPlus()

class MessageBuilder {
    private val lines = mutableListOf<String>()

    operator fun String.unaryPlus() {
        lines += this
    }

    fun build(): String {
        return lines.joinToString("\n")
    }
}

fun message(block: MessageBuilder.() -> Unit): String {
    return MessageBuilder()
        .apply(block)
        .build()
}

使用:

fun main() {
    val text = message {
        +"订单编号:1001"
        +"支付状态:已支付"
        +"配送状态:待发货"
    }

    println(text)
}

输出:

订单编号:1001
支付状态:已支付
配送状态:待发货

这里的:

+"订单编号:1001"

会在 MessageBuilder 作用域内调用:

"订单编号:1001".unaryPlus()

这种写法适合边界明确的 DSL。普通业务代码如果大量出现孤立的 +"text",反而会增加理解成本。

哪些运算符不能重载

Kotlin 不允许所有符号都参与重载。

常见不可重载语法包括:

  • &&||
  • ?:
  • ===!==
  • is!is
  • asas?
  • =
  • .?.
  • ::

&&|| 不能重载,一个重要原因是短路求值。

conditionA() && conditionB()

如果 conditionA()falseconditionB() 根本不会执行。

普通函数调用通常要先计算参数。如果允许把 && 简单映射成函数,短路语义就容易被破坏。

=== 也不能重载,因为它明确表示引用是否相同,不应该由业务类型重新解释。

运算符重载几乎不等于额外反射开销

表达式:

val result = a + b

由编译器静态解析为对应函数调用,不需要运行时反射查找。

因此运算符写法和直接调用 a.plus(b) 通常没有本质性能差异。

真正需要关注的是运算符函数内部做了什么:

  • 是否创建大量新对象
  • 是否复制大集合
  • 是否执行数据库或网络操作
  • 是否存在复杂算法
  • 是否修改共享状态

符号很短,不代表执行成本很低。

例如下面的设计虽然语法允许,但非常不合适:

operator fun User.plus(role: Role): User {
    database.insertUserRole(id, role.id)
    return this
}

看到 user + role,很难想到它会写数据库。

更清楚的写法:

fun User.assignRole(role: Role) {
    database.insertUserRole(id, role.id)
}

运算符设计的五条原则

符号语义必须符合直觉

适合:

moneyA + moneyB
vector * scalar
item in container
matrix[row, column]

不适合:

user + role       // 实际写数据库
order - customer  // 实际取消订单
!service          // 实际重启服务
plus 优先返回新对象

+ 通常给人“计算一个新结果”的预期。

val c = a + b

如果这行代码偷偷修改 a,后续逻辑很容易出错。

不可变值对象最适合重载算术运算符。

可变操作要明显

确实需要原地修改时,可以使用 plusAssign,但类型本身应该清楚地表现出可变性,例如 MutableCartMutableMatrix

compareTo、equals、hashCode 保持一致

相等、排序和哈希集合使用的是不同约定。三者含义冲突时,SetMapTreeSet 和排序结果都可能变得反直觉。

不要为了少写几个字符牺牲业务含义
repository(user)

虽然比:

repository.save(user)

短,但动作含义消失了。

符号适合稳定、通用、接近数学或容器直觉的操作。带副作用、成本高、失败模式复杂的业务动作,命名函数通常更可靠。

一个完整综合 Demo

下面用库存对象串起 plusminuscontainsgetinvoke

data class Product(
    val sku: String,
    val name: String
)

data class StockItem(
    val product: Product,
    val quantity: Int
)

class Inventory private constructor(
    private val quantities: Map<Product, Int>
) {
    companion object {
        fun empty(): Inventory {
            return Inventory(emptyMap())
        }
    }

    operator fun plus(item: StockItem): Inventory {
        require(item.quantity > 0) { "入库数量必须大于 0" }

        val current = quantities[item.product] ?: 0
        return Inventory(
            quantities + (item.product to (current + item.quantity))
        )
    }

    operator fun minus(item: StockItem): Inventory {
        require(item.quantity > 0) { "出库数量必须大于 0" }

        val current = quantities[item.product] ?: 0
        require(current >= item.quantity) {
            "库存不足:${item.product.sku},当前 $current,需要 ${item.quantity}"
        }

        val remaining = current - item.quantity
        val newQuantities = if (remaining == 0) {
            quantities - item.product
        } else {
            quantities + (item.product to remaining)
        }

        return Inventory(newQuantities)
    }

    operator fun contains(product: Product): Boolean {
        return get(product) > 0
    }

    operator fun get(product: Product): Int {
        return quantities[product] ?: 0
    }

    operator fun invoke(): List<StockItem> {
        return quantities.map { (product, quantity) ->
            StockItem(product, quantity)
        }
    }
}

使用:

fun main() {
    val keyboard = Product("K001", "Keyboard")
    val mouse = Product("M001", "Mouse")

    var inventory = Inventory.empty()

    inventory += StockItem(keyboard, 10)
    inventory += StockItem(mouse, 5)
    inventory -= StockItem(keyboard, 3)

    println(keyboard in inventory)
    println(inventory[keyboard])
    println(inventory[mouse])
    println(inventory())
}

输出:

true
7
5
[StockItem(product=Product(sku=K001, name=Keyboard), quantity=7), StockItem(product=Product(sku=M001, name=Mouse), quantity=5)]

这里没有定义 plusAssignminusAssign

所以:

inventory += item
inventory -= item

分别退化成:

inventory = inventory + item
inventory = inventory - item

Inventory 自身保持不可变,每次入库和出库都返回新对象。变量使用 var,只是为了接住新结果。

常见误区

operator 可以加在任意函数名上

不可以。

函数名和参数形式必须符合 Kotlin 已定义的约定。operator fun add(...) 不会获得 + 语法。

a in b 调用 a.contains(b)

正好相反,它调用 b.contains(a)

===== 都能重载

只有 == 会使用可重写的 equals=== 固定比较对象引用,不能重载。

compareTo 必须返回 -1、0、1

不需要。只要负数、零、正数的方向正确即可。

a += b 一定调用 plusAssign

不一定。没有适用的 plusAssign 时,可以退化成 a = a + b。两套函数同时适用时还可能产生歧义。

运算符函数一定没有副作用

语言没有强制限制,但 API 设计应该遵守直觉。plusminustimes 最好返回新值,明显的可变操作再使用 plusAssign 等函数。

区间能判断 in,就一定能 for 遍历

不一定。成员判断依赖 contains,遍历依赖 iteratorhasNextnext,是两套不同约定。

总结

Kotlin 运算符重载的本质不是符号魔法,而是编译器约定的函数调用:

a + b       -> a.plus(b)
a[i]        -> a.get(i)
a in b      -> b.contains(a)
a()         -> a.invoke()
a..b        -> a.rangeTo(b)
a < b       -> a.compareTo(b) < 0

最容易混淆的是复合赋值:

plusAssign  修改当前对象
plus        返回新对象,再由变量接住新值

适合运算符重载的类型通常具备稳定、直观的运算含义:

  • 金额
  • 向量和矩阵
  • 区间
  • 容器
  • 索引结构
  • 可执行规则
  • 小型 DSL

一句话概括:

运算符重载的价值不是把函数名变短,而是让类型的行为更接近它所表达的概念;符号无法准确表达业务动作时,普通命名函数反而更清楚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值