[关闭]
@zhongzilu 2019-01-28T12:16:14.000000Z 字数 4113 阅读 1625

Kotlin 中操算符的重载

Kotlin 做为一门新晋的编程语言, 继承了众多编程语言的优秀特性, 你能从它身上看到很多其他编程语言的影子, 其他编程语言中的常见操作符, Kotlin 肯定也少不了, 比如一元操算符 ( a++ ), 二元操算符 ( a + b ), 数组操作符 ( array[0] )等, 同时 Kotlin 允许我们在自定义的类中重载这些操作符.

本篇文章重点讲解以下几个方面, 让大家对 Kotlin 中的操算符重载, 以及如何运用有所了解.

  • 它是什么?
  • 它的优点是什么?
  • 它的原理是什么?
  • Kotlin 中有哪些操作符?
  • 如何使用?

它是什么?

操作符的重载, 和类中方法函数的重载相似, Kotlin 和其他编程语言一样, 提供了一系列的操作符, 但不同的是, Kotlin 还提供了一套对应的操作符方法函数.

简单的来说, 操作符的重载, 就是重载这些操作符对应的方法函数, 使用operator关键字来进行修饰.

比如 Kotlin 中两个字符串的拼接, 就是 String 类重载了加号操作符对应的方法函数plus(), 重载代码如下:

  1. public operator fun plus(other: Any?): String

在使用的时候和常见的两个字符串拼接一样, 如下:

  1. "String1" + "String2"

它的优点是什么?

我们知道, 在 Java 中能使用加号操作符的场景就只有两个地方, 一个是进行数学运算, 另一个就是进行字符串拼接, 其他场景就没法用了, 那么 Kotlin 中操作符重载的第一个优点就有了, 它可以应用到任何类当中, 包括自定义的类.

由于是方法函数重载, 因此, 他第二个优点就是可以自定义操作符的运算规则.

这些优点会在本文[ 如何应用 ]章节中的案例代码中体现出来.

⚠️注意: 恰当地使用操作符重载可以增加代码可读性, 使代码更加简洁, 但不恰当地使用操作符重载会增加源码阅读障碍, 还可能造成错误地理解. 请三思而后行!!!

实现原理是什么?

它的实现原理说来也简单, 就是编译器在编译的时候, 将操作符的表达式翻译成对应操作符的方法调用, 比如字符串拼接的表达式:

  1. "String1" + "String2"

翻译后就成了调用加号操作符的对应方法函数plus(), 代码大概如下:

  1. String("String1").plus(String("String2"))

⚠️注意, 由于 String 类是基础类型, 因此字符串拼接的实现不是上面翻译后代码那样, 而是针对基础类型做了优化, 所有的操作符重载都针对基础类型做了优化,不会存在函数调用的开销。这里只是为了方便举例说明操作符重载的原理才这样写的

有哪些操作符?

下面罗列了 Kotlin 中一系列的操作符对应方法的表

一元操作符

操作符 对应方法函数
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a++ a.inc()
a-- a.dec()

二元操作符

操作符 对应方法函数
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)、a.mod(b)
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)、a.modAssign(b)

⚠️注意: Kotlin 1.0 使用mod操作符, 自 Kotlin 1.1 弃用了mod操作符, 改为支持rem操作符

“In”操作符

操作符 对应方法函数
a in b b.contains(a)
a !in b !b.contains(a)

In 操作符会被转换为调用合适的 contains 方法函数, 该操作符会在集合区间相关文章中出现

In 操作符可以用来遍历数组或集合或区间, 也可以用来判断集合或 Map 中是否包含某个元素, 再或者是判断字符串中是否包含某个字符, 或一个区间中是否包含某个数值等.

区间操作符

操作符 对应方法函数
a..b a.rangeTo(b)

区间操作符 .. 会被转换成调用 rangeTo 方法函数.

区间操作符 .. 是一个数学意义上的闭区间概念, 即 ab 的闭区间, 该操作符通常会和 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] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

方括号会被转换为调用带有适当数量参数的getset方法函数

函数调用操作符

操作符 对应方法函数
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 == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

相等操作符有些特殊:它被翻译成⼀个复杂的表达式,⽤于筛选 null 值。null == null 总是 true,对于⾮空的 aa == null 总是 false ⽽不会调⽤ a.equals()

由于需要得到一个确切的函数结构比较, 所以重载时不仅仅需要方法函数名相同, 而且必须要如下准确的被实现:

  1. operator fun equals(other: Any?): Boolean

⚠️注意: ===!== 操作不可被重载, ( 他们分别相当于 Java 中的 ==!= )

比较操作符

操作符 对应方法函数
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

所有的比较都转换为调用compareTo()方法函数, 这个函数需要返回Int

属性委托操作符

provideDelegategetValue 以及 setValue 操作符函数请查看委托属性章节的文章

中缀调用操作符

命名函数的中缀调用操作符请查看中缀调用章节的文章

如何使用?

重载操作符的方式有两种, 第一种是在我们自定义的类中, 重载需要重载的操作符对应的方法函数, 比如上文中的加号操作符, 它对应的操作符方法函数名就是plus(), 操作符对应的方法函数名, 可以查阅本文中的[ 有哪些操作符? ]章节;

第二种是使用 Kotlin 的扩展函数来给类重载操作符.

下面我们通过示例来看看怎么重载操作符. 假设我们有一个自定义的数据类 Point, 用来表示屏幕上的一个点, 数据类代码如下:

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

现在有个需求, 需要反转点坐标, 普通的写法代码大概如下:

  1. var point = Point(10, 20)
  2. //进行坐标反转
  3. point = Point(-point.x, -point.y)
  4. println(point) // 输出“Point(x=-10, y=-20)”

看上去有点繁琐, 我们可以通过重载一元减号操作符来简化此操作, 修改一下Point类, 修改后的代码如下:

  1. // 方式一: 在类中进行重载
  2. data class Point(val x: Int, val y: Int) {
  3. // 重载一元减号操作符
  4. operator fun unaryMinus() = Point(-x, -y)
  5. }
  6. // 方式二: 使用扩展函数进行重载
  7. operator fun Point.unaryMinus() = Point(-x, -y)

进行坐标反转时就可以如下写:

  1. var point = Point(10, 20)
  2. println(-point) // 输出“Point(x=-10, y=-20)”

重载了操作符后是不是使用起来更简单了呢? 而且看上去也很符合我们的直觉思维.

如果此时新增一个需求, 需要进行两个点相加来计算坐标, 普通方式的写法大概如下:

  1. val point1 = Point(10, 20)
  2. val point2 = Point(20, 30)
  3. //进行两个点坐标相加
  4. val x = point1.x + point2.x
  5. val y = point1.y + point2.y
  6. val point3 = Point(x, y)
  7. println(point3) //输出“Point(x=30, y=50)”

看上去是不是很麻烦? 如果我们通过重载二元加号运算符的话, 就不会那么麻烦了, 我们再次修改Point类代码:

  1. // 方式一: 在类中进行重载
  2. data class Point(val x: Int, val y: Int) {
  3. // 重载一元减号操作符
  4. operator fun unaryMinus() = Point(-x, -y)
  5. // 重载二元加号操作符
  6. operator fun plus(point: Point) = Point(x + point.x, y + point.y)
  7. }
  8. // 方式二: 使用扩展函数进行重载
  9. operator fun Point.unaryMinus() = Point(-x, -y)
  10. operator fun Point.plus(point: Point) = Point(x + point.x, y + point.y)

那么, 此时我们进行两个点相加, 代码就可以这样写:

  1. val point1 = Point(10, 20)
  2. val point2 = Point(20, 30)
  3. // 进行两个点相加
  4. println(point1 + point2) //输出“Point(x=30, y=50)”

这样写是不是更直观呢? 好了, 本篇文章也就到此结束了, 相信大家看完后都对 Kotlin 中重载操作符有了一定的了解, 也学到了如何去重载操作符, 这里给大家推荐一个开源库 Kolley, 库中就使用了重载二元减号操作符的方式, 来简化添加网络请求参数的方法.

总结

最后给大家总结一下, 重载操作符就是重载 Kotlin 预定义的一系列的操作符方法函数, 通过 operator 关键字进行修饰. 重载的方式可以是在类中进行重载, 或是使用扩展函数的方式进行重载, 它的优点就是可以在任何类中很方便的进行重载, 使得代码编写更简洁, 更直观, 更符合人们的直觉思维, 但过度使用或不恰当使用, 会给代码维护和阅读增加阻碍, 所以需要三思而后行.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注