@zhongzilu
2019-01-28T20:16:14.000000Z
字数 4113
阅读 1905
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.x
val y = point1.y + point2.y
val 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
关键字进行修饰. 重载的方式可以是在类中进行重载, 或是使用扩展函数的方式进行重载, 它的优点就是可以在任何类中很方便的进行重载, 使得代码编写更简洁, 更直观, 更符合人们的直觉思维, 但过度使用或不恰当使用, 会给代码维护和阅读增加阻碍, 所以需要三思而后行.