@zhongzilu
2019-01-28T12:16:14.000000Z
字数 4113
阅读 2407
Kotlin 做为一门新晋的编程语言, 继承了众多编程语言的优秀特性, 你能从它身上看到很多其他编程语言的影子, 其他编程语言中的常见操作符, Kotlin 肯定也少不了, 比如一元操算符 ( a++ ), 二元操算符 ( a + b ), 数组操作符 ( array[0] )等, 同时 Kotlin 允许我们在自定义的类中重载这些操作符.
本篇文章重点讲解以下几个方面, 让大家对 Kotlin 中的操算符重载, 以及如何运用有所了解.
- 它是什么?
- 它的优点是什么?
- 它的原理是什么?
- Kotlin 中有哪些操作符?
- 如何使用?
操作符的重载, 和类中方法函数的重载相似, Kotlin 和其他编程语言一样, 提供了一系列的操作符, 但不同的是, Kotlin 还提供了一套对应的操作符方法函数.
简单的来说, 操作符的重载, 就是重载这些操作符对应的方法函数, 使用operator关键字来进行修饰.
比如 Kotlin 中两个字符串的拼接, 就是 String 类重载了加号操作符对应的方法函数plus(), 重载代码如下:
public operator fun plus(other: Any?): String
在使用的时候和常见的两个字符串拼接一样, 如下:
"String1" + "String2"
我们知道, 在 Java 中能使用加号操作符的场景就只有两个地方, 一个是进行数学运算, 另一个就是进行字符串拼接, 其他场景就没法用了, 那么 Kotlin 中操作符重载的第一个优点就有了, 它可以应用到任何类当中, 包括自定义的类.
由于是方法函数重载, 因此, 他第二个优点就是可以自定义操作符的运算规则.
这些优点会在本文[ 如何应用 ]章节中的案例代码中体现出来.
⚠️注意: 恰当地使用操作符重载可以增加代码可读性, 使代码更加简洁, 但不恰当地使用操作符重载会增加源码阅读障碍, 还可能造成错误地理解. 请三思而后行!!!
它的实现原理说来也简单, 就是编译器在编译的时候, 将操作符的表达式翻译成对应操作符的方法调用, 比如字符串拼接的表达式:
"String1" + "String2"
翻译后就成了调用加号操作符的对应方法函数plus(), 代码大概如下:
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 += b | a.plusAssign(b) |
| a -= b | a.minusAssign(b) |
| a *= b | a.timesAssign(b) |
| a /= b | a.divAssign(b) |
| a %= b | a.remAssign(b)、 |
⚠️注意: Kotlin 1.0 使用mod操作符, 自 Kotlin 1.1 弃用了mod操作符, 改为支持rem操作符
| 操作符 | 对应方法函数 |
|---|---|
| a in b | b.contains(a) |
| a !in b | !b.contains(a) |
In 操作符会被转换为调用合适的 contains 方法函数, 该操作符会在集合和区间相关文章中出现
In 操作符可以用来遍历数组或集合或区间, 也可以用来判断集合或 Map 中是否包含某个元素, 再或者是判断字符串中是否包含某个字符, 或一个区间中是否包含某个数值等.
| 操作符 | 对应方法函数 |
|---|---|
| a..b | a.rangeTo(b) |
区间操作符 .. 会被转换成调用 rangeTo 方法函数.
区间操作符 .. 是一个数学意义上的闭区间概念, 即 a 到 b 的闭区间, 该操作符通常会和 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) |
方括号会被转换为调用带有适当数量参数的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 == b | a?.equals(b) ?: (b === null) |
| a != b | !(a?.equals(b) ?: (b === null)) |
相等操作符有些特殊:它被翻译成⼀个复杂的表达式,⽤于筛选 null 值。null == null 总是 true,对于⾮空的 a ,a == null 总是 false ⽽不会调⽤ a.equals() 。
由于需要得到一个确切的函数结构比较, 所以重载时不仅仅需要方法函数名相同, 而且必须要如下准确的被实现:
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值
provideDelegate 、getValue 以及 setValue 操作符函数请查看委托属性章节的文章
命名函数的中缀调用操作符请查看中缀调用章节的文章
重载操作符的方式有两种, 第一种是在我们自定义的类中, 重载需要重载的操作符对应的方法函数, 比如上文中的加号操作符, 它对应的操作符方法函数名就是plus(), 操作符对应的方法函数名, 可以查阅本文中的[ 有哪些操作符? ]章节;
第二种是使用 Kotlin 的扩展函数来给类重载操作符.
下面我们通过示例来看看怎么重载操作符. 假设我们有一个自定义的数据类 Point, 用来表示屏幕上的一个点, 数据类代码如下:
data class Point(val x: Int, val y: Int)
现在有个需求, 需要反转点坐标, 普通的写法代码大概如下:
var point = Point(10, 20)//进行坐标反转point = Point(-point.x, -point.y)println(point) // 输出“Point(x=-10, y=-20)”
看上去有点繁琐, 我们可以通过重载一元减号操作符来简化此操作, 修改一下Point类, 修改后的代码如下:
// 方式一: 在类中进行重载data class Point(val x: Int, val y: Int) {// 重载一元减号操作符operator fun unaryMinus() = Point(-x, -y)}// 方式二: 使用扩展函数进行重载operator fun Point.unaryMinus() = Point(-x, -y)
进行坐标反转时就可以如下写:
var point = Point(10, 20)println(-point) // 输出“Point(x=-10, y=-20)”
重载了操作符后是不是使用起来更简单了呢? 而且看上去也很符合我们的直觉思维.
如果此时新增一个需求, 需要进行两个点相加来计算坐标, 普通方式的写法大概如下:
val point1 = Point(10, 20)val point2 = Point(20, 30)//进行两个点坐标相加val x = point1.x + point2.xval y = point1.y + point2.yval point3 = Point(x, y)println(point3) //输出“Point(x=30, y=50)”
看上去是不是很麻烦? 如果我们通过重载二元加号运算符的话, 就不会那么麻烦了, 我们再次修改Point类代码:
// 方式一: 在类中进行重载data class Point(val x: Int, val y: Int) {// 重载一元减号操作符operator fun unaryMinus() = Point(-x, -y)// 重载二元加号操作符operator fun plus(point: Point) = Point(x + point.x, y + point.y)}// 方式二: 使用扩展函数进行重载operator fun Point.unaryMinus() = Point(-x, -y)operator fun Point.plus(point: Point) = Point(x + point.x, y + point.y)
那么, 此时我们进行两个点相加, 代码就可以这样写:
val point1 = Point(10, 20)val point2 = Point(20, 30)// 进行两个点相加println(point1 + point2) //输出“Point(x=30, y=50)”
这样写是不是更直观呢? 好了, 本篇文章也就到此结束了, 相信大家看完后都对 Kotlin 中重载操作符有了一定的了解, 也学到了如何去重载操作符, 这里给大家推荐一个开源库 Kolley, 库中就使用了重载二元减号操作符的方式, 来简化添加网络请求参数的方法.
最后给大家总结一下, 重载操作符就是重载 Kotlin 预定义的一系列的操作符方法函数, 通过 operator 关键字进行修饰. 重载的方式可以是在类中进行重载, 或是使用扩展函数的方式进行重载, 它的优点就是可以在任何类中很方便的进行重载, 使得代码编写更简洁, 更直观, 更符合人们的直觉思维, 但过度使用或不恰当使用, 会给代码维护和阅读增加阻碍, 所以需要三思而后行.