Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理系列文章
Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数
一、本文概要
本文是对<<Kotlin in Action>>
的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:
在
Kotlin
中,我们可以通过 调用自己代码中定义的函数,来实现 特定语言结构。这些功能与 特定的函数命名 相关,而不是与特定的类型绑定。例如,如果在你的类中定义了一个名为plus
的特殊方法,那么按照约定,就可以在该类的实例上使用+
运算符,这种技术称为 约定。
因为由类实现的接口集是固定的,而Kotlin
不能为了实现其他接口而修改现有的类,因此一般 通过扩展函数的机制 来为现有的类增添新的 约定方法,从而适应任何现有的Java
类。
二、重载算术运算符
在Kotlin
中,使用约定的最直接的例子就是 算术运算符,在Java
中,全套的算术运算符只能用于基本数据类型,+
运算符可以与String
一起使用。下面,我们看一下在Kotlin
中,如何使用算术运算符来完成一些其它的事情。
2.1 重载二元运算符
假设已经有一个数据类Point
,它包含两个成员变量,分别是x,y
点的坐标值,我们希望通过算术运算符+
对两个Point
对象相加之后,能够得到一个新的Point
对象,它的成员变量x,y
为原有两个Point
对象的x,y
之和。
运行结果为:
在上面的代码中,我们为
Point
类定义了一个扩展函数plus
,这样当我们调用first + second
,实际上执行的是first.plus(second)
方法来得到一个新的Point
对象。这里需要注意的是:用于重载运算符的所有函数都需要 用 operator 关键字来标记,用来表示你打算 把这个函数作为相应的约定的实现。
所有可重载的二元算术运算符如下,自定义类型的运算符,基本上和标准数字类型的运算符有着相同的优先级。
-
a * b
:times
-
a / b
:div
-
a % b
:mod
-
a + b
:plus
-
a - b
:minus
运算符函数和 Java
- 当从
Java
调用Kotlin
运算符非常容易,只需要像普通函数一样调用即可,例如上面的plus
方法。 - 当从
Kotlin
调用Java
的时候,对于与Kotlin
约定匹配的函数(不要求使用operator
修饰符,但是参数需要匹配名称和数量)都可以使用运算符语言来调用。如果Java
类定义了一个满足需求的函数,但是起了一个不同的名称,可以通过定义一个扩展函数来修正这个函数名用来替代现有的Java
方法。
没有用于位运算的特殊运算符
Kotlin
没有为标准数字类型Int
,Long
等定义任何位运算符,因此也不允许你为自定类型定义它们。相反,它使用中缀调用语法的函数,可以为自定义类型定义相似的函数,下面我们为Point
添加一个and
,用于执行位运算。
运行结果为:
这里我们不再使用
operator
关键字来声明,而是用infix
来定义一个中缀调用语法的函数,其它执行位运算的函数包括:shl
、shr
、ushr
、and
、or
、xor
和inv
。
2.2 重载复合赋值运算符
当在定义像plus
这样的函数,Kotlin
不止支持+
号运算,也支持像+=
这样的 复合赋值运算符。
需要注意,这个只对于可变变量有效,也就是
first
要声明为var
。在一些情况下,定义+=
运算符可以 修改使用它的变量所引用的对象,但不会重新分配引用,将一个元素添加到可变集合,就是一个很好的例子:
如果你定义了一个返回值为
Unit
,名为plusAssign
的函数,Kotlin
将会在用到+=
运算符的地方使用它,其它二元运算符也有命名相似的对应函数:minusAssign
、timesAssign
等。
当在代码中用到+=
的时候,理论上plus
和plusAssign
都可能会被调用,如果两个函数都有定义并且适用,那么编译器就会报错,例如下面这样的定义:
编译时的错误为:
解决方法有两种:
- 使用 不可变 val 代替可变 var 来修饰
first
,这样plus
运算符就不再适用。 - 不要同时为一个类添加
plus
和plusAssign
运算。如果一个类是 不可变的,那就应该只提供返回一个新值的运算;如果一个类是 可变的,例如构建器,那么只需要提供plusAssign
和类似的运算符就够了。
Kotlin
的标准库支持集合的这两种方法:
-
+
和-
运算符总是返回一个新的集合 -
+=
和-=
运算符用于可变集合时,始终在一个地方修改它们;而它们用于只读集合时,会返回一个修改过的副本。
作为它们的运算数,可以使用单个元素,也可以使用元素类型一致的其它集合:
运行结果为:
2.3 重载一元运算符
重载一元运算的过程和前面看到的方式相同:用预先定义的一个名称来声明函数,并用修饰符operator
标记。下面的例子中重载了-a
运算符:
运行结果为:
所有可重载的一元算法运算符包括:
-
+a
:unaryPlus
-
-a
:unaryMinus
-
!a
:not
-
++a/a++
:inc
-
--a/a--
:dec
当你定义inc
和dec
函数来重载自增和自减的运算符时,编译器自动支持与普通数字类型的前缀、后缀自增运算符相同的语义。例如后缀运算会先返回变量的值,然后才执行++
操作。
三、重载比较运算符
与算术运算符一样,在Kotlin
中,可以对任何对象使用比较运算符(==
、!=
、>
和<
),而不仅仅限于基本数据类型。
3.1 等号运算符,equals
如果在Kotlin
中使用==/!=
运算符,它将被转换成equals
方法的调用,和其他运算符不同的是,==
和!=
可以用于可空运算数,比较a == b
会检查a
是否为飞空,如果不是就调用a.equals(b)
,完整的调用如下所示:
a?.equals(b) ?: (b == null)
对于data
修饰的数据类,equals
的实现将会由编译器自动生成,如果需要手动实现,可以参考下面的做法:
- 比较是否指向同一对象的引用,如果是,那么直接返回
true
- 类型如果不同,直接返回
false
- 比较作为判断依据的字段
equals
函数之所以被标记为override
,这是因为这个方法的实现是在Any
类中定义的,而operator
关键字在基本方法中已经标记了。同时,equals
不能实现为扩展函数,因为继承自Any
类的实现始终优先于扩展函数。
3.2 排序运算符 compareTo
在Kotlin
中,对于实现了Comparable
接口中定义的compareTo
方法的类可以按约定调用,比较运算符<、>、<=、>=
的使用将被转换为compareTo
,compareTo
的返回类型必须为int
,也就是说p1 < p2
表达式等价于p1.compareTo(p2) < 0
。
下面,我们定义一个Person
类,让其根据年龄来比较大小:
运行结果为:
在上面的例子中,我们用到了
Kotlin
标准库函数中的compareValuesBy
函数来简洁地实现compareTo
方法,这个函数 接收用来计算比较值的一系列回调,按顺序依次调用回调方法,两两一组分别做比较:
- 如果值不同,则返回比较结果
- 如果相同,则继续调用下一个
- 如果没有更多的回调来调用,则返回
0
这些回调函数可以像lambda
一样传递,或者像这里做的一样,作为属性引用传递。
四、集合与区间的约定
处理集合最常见的操作包含两种:
- 通过下标来获取和设置元素,使用语法
a[b]
,称为 下标运算符。 - 检查元素是否属于当前集合,使用
in
运算符。
4.1 通过下标来访问元素:get 和 set
在Kotlin
中,下标运算符是一种约定,使用下标运算符读取元素会被转换为get
运算符方法的调用,并且写入元素将调用set
,下面我们为Point
类添加类似的方法:
get
的参数可以是任何类型,而不止是Int
,例如,当你对map
使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义具有多个参数的get
方法,例如如果要实现一个类来表示二维数组或矩阵,你可以定义一个方法,例如operator fun get(rowIndex : Int, colIndex : Int)
,然后用matrix[row, col]
来调用。
下面,我们再来看一下set
的约定方法:
运行结果为:
定义
set
函数后,就可以在赋值语句中使用下标运算符,set
的最后一个参数用来接收赋值语句中(等号)右边的值,其他参数作为方括号内的下标。
4.2 in 的约定
集合支持的另一个运算符是in
运算符,用于检查某个对象是否属于集合,相应的函数叫做contains
,下面的例子用于判断某个点是否处于矩形范围之内:
运行结果为:
4.3 rangeTo 的约定
要创建一个区间时,使用的是..
语法,例如1..10
代表所有从1
到10
的数字,..
运算符是调用rangeTo
函数的一个简洁方法。rangeTo
返回一个区间,你可以为自己的类定义这个运算符,但是,如果该类实现了Comparable
接口,那么就不需要了,你可以通过Kotlin
标准库创建一个任意可比较元素的区间,这个库定义了可以用于任何可比较元素的rangeTo
函数
operator fun <T : Comparable<T>> T.rangeTo(that : T) : ClosedRange<T>
这个函数返回一个区间ClosedRanged
,可以用来检测其它一些元素是否属于它。
作为例子,我们用LocalData
来构建一个日期的区间:
运行结果为:
上面的
now..now.plusDays(10)
将会被编译器转换为now.rangeTo(now.plusDays(10))
,它并不是LocalDate
的成员函数,而是Comparable
的一个扩展函数。
4.4 在 "for" 循环中使用 "iterator" 的约定
在for
循环中使用in
运算符表示 执行迭代操作,诸如for(x in list) { }
将被转换成list.iterator()
的调用,然后在上面重复调用hasNext
和next
方法。
运行结果为:
上面用到了 Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字 中介绍的通过
object
来实现匿名内部类的知识。
五、解构声明和组件函数
解构声明的功能允许你展开单个复合值,并使用它来初始化多个单独的变量。它再次用到了约定的原理,要在解构声明中初始化每个变量,将调用名为componentN
的函数,其中N
是声明中变量的位置。
对于数据类,编译器为每个在主构造方法中声明的属性生成一个componentN
函数,下面的例子显示了如何手动为非数据类声明这些功能:
运行结果为:
解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。如果要这样做,可以定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。在调用函数之后,可以用解构声明的方式,来轻松的展开它,使用其中的值。
解构声明不仅可以用作函数中的顶层语句,还可以用在其他可以声明变量的地方,例如使用in
循环来枚举map
中的条目:
运行结果为:
更多文章,欢迎访问我的 Android 知识梳理系列:
- Android 知识梳理目录:http://www.jianshu.com/p/fd82d18994ce
- 个人主页:http://lizejun.cn
- 个人知识总结目录:http://lizejun.cn/categories/