重载运算符
在Kotlin中,可以通过定义特定名字的函数来实现运算符的重载,这种方式被称为约定。
算术运算符
使用约定的最直接的例子就是算术运算符:
data class Point(var x:Int,var y:Int) {
operator fun plus(other: Point):Point {
return Point(x + other.x, y + other.y)
}
}
val point1 = Point(1,2)
val point2 = Point(3,4)
println(point1 + point2) //输出:Point(x=4, y=6)
用于重载的函数都需要使用关键字 operator 修饰,像上面这样定义了plus函数后,我们就可以使用 + 号给两个Point求和了,但事实上,这个时候它调用的是plus函数。
除了把这个运算符声明为一个成员函数外,也可以把它定义为一个扩展函数。上面的plus函数和下面这个是等价的:
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
虽然重载函数都需要operator关键字,但并不意味着我们可以用operator重载任意运算符或者用随意名字命名重载函数。事实上, Kotlin 限定了我们能重载哪些运算符,以及需要在类中定义的对应名字的函数。下面这个表列举了我们可以重载的函数以及对应的函数名:
表达式 | 函数名 |
---|---|
a * b | times |
a / b | div |
a % b | rem |
a + b | plus |
a - b | minus |
自定义类型的运算符,基本上和与标准数字类型的运算符有着相同的优先级。例如,如果是 a + b * c,乘法将始终在添加之前执行, 即使是自己重载了这些运算符。运算符 * 、/和%具有相同的优先级,高于+和 - 运算符的优先级。
定义运算符的时候,不要求两个运算符是相同的类型,函数的返回值也可以是任意类型(可以不同于任一运算符参数)。甚至我们可以像对普通函数一样重载多个同名的,但参数类型不同的operator函数。
除了上面已经列举的运算符,Kotlin还支持一元运算符,和复合赋值运算符,下面是这些运算符的列表:
表达式 | 函数名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
a++ | inc |
a- - | dec |
a += b | plusAssign |
a -= b | minusAssign |
a *= b | timesAssign |
a /= b | divAssign |
a %= b | remAssign |
比较运算符
和算法运算符一样,我们同样可以重载比较运算符。
等号运算符
我们可以重载等号运算符使得对任何对象都能使用 == 和 != ,重载这两个运算符,我们只需要定义一个equals方法—— == 会调用equals方法,而 != 则会调用equals方法并返回方法结果的相反值。与算法运算符不同的是,等号运算符可以调用被用于可空参数,因为这些运算符会检查参数是否为空,当两个参数都为null时会返回true:
data class Point(var x:Int, var y:Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
return x == other.x && y == other.y
}
}
var point1 = Point(1,2)
var point2 = Point(1,2)
println(point1 == point2) //true
println(null == point1) //false
在上面的代码中可以看到,我们使用了override标记equals而不是operator,这是因为equals方法早已经在Any类中就定义了实现,所以我们需要使用override而可以省略已经在Any中标记了的operator。此外,我们还使用了 === (恒等于)来比较参数和调用方法的对象是否有相同的引用(或者当调用者是基本类型的时候。是否是相同的值)。注意,=== 和 !== (与===相反)不可重载,因此不存在对他们的约定。
排序运算符
所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值:
data class Point(var x:Int, var y:Int) {
operator fun compareTo(other: Point):Int {
return x + y - other.x - other.y
}
}
var point1 = Point(1,2)
var point2 = Point(4,2)
println(point1 < point2)//false
point1 < point2 表达式等价于 point1.compareTo(point2) < 0,其余的 > 、<= 、>= 等运算符的处理也都与此相同。
集合和区间的约定
处理集合最常见的一些操作是通过下标来获取和设置元素,以及检查元素是否属于当前集合。所有的这些操作都支持运算符语法:要通过下标获取或设置元素 ,可以使用语法 a [b] (称为下标运算符) 。 可以使用 in 运算符来检查元素是否在集合或区间内,也可以迭代集合。
get 和 set
在 Kotlin 中,使用下标运算符读取元素会被转换为 get 运算符方法的调用 ,井且写入元素将调用 set:
data class Point(var x:Int, var y:Int) {
operator fun get(index: Int):Int = when(index) {
1 -> x
2 -> y
else ->
throw IndexOutOfBoundsException ("Invalid parameter $index")
}
operator fun set(index: Int,value: Int) = when(index) {
1 -> x = value
2 -> y = value
else ->
throw IndexOutOfBoundsException ("Invalid parameter $index")
}
}
var point = Point(1,2)
println(point[1]) //1
point[1] = point[2]
println(point[1]) //2
在上面的代码中,point[1] 被转换成了 point.get(1),而 point[1] = point[2] 被转换成了 point.set(1,point.get(2))。get 的参数可以是任何类型,而不只是 Int ,还可以定义具有多个参数的 get 方法;set 的最后一个参数用来接收赋值语句中(等号)右边的值,其他参数作为方括号内的下标。
in操作符
in运算符用作区间或集合检查时 。 相应的函数叫作 contains, !in对应的则是!contains。
for 循环中也可以使用 in 运算符,这时它被用来执行迭代:一个诸如 for(x in list) {...}将被转换成 list.iterator ()的调用,然后就像在 Java 中一样,在它上面重复调用 hasNext 和 next 方法。 这么说有点复杂,举个例子:String 的父类CharSequence 重载了 iterator:
public operator fun CharSequence.iterator(): CharIterator = object : CharIterator() {
private var index = 0
public override fun nextChar(): Char = get(index++)
public override fun hasNext(): Boolean = index < length
}
CharIterator是一个声明了next 和hasNext 的 抽象类。 因此iterator事实上是需要用对象表达式来重载的。
解构声明
解构声明在前面的《Kotlin自学之旅(四)函数、扩展函数》已经提到。结构声明用到了约定的原理,要在解构声明中初始化每个变量,将调用名为 componentN 的函数,其中 N 是声明中变量的位置。换句话说,一个解构声明可以表示成下图的样子:
解构声明主要使用场景之一,是从一个函数返回多个值,解构声明不仅可以用作函数中的顶层语句,还可以用在其他可以声明变量的地方,例如 in 循环。当然,不可能定义无限数量的 componentN 函数,标准库只 允许使用此语法来访问一个对象的前五个元素。
委托属性
在《Kotlin自学之旅(六)对象和委托》中,我们已经知道了怎么使用委托对象,现在我们来看看怎么使用委托属性。委托属性的基本语法如下:
class Example {
var p: String by Delegate()
}
编译器创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始属性 p 会委托给该实例。原理类似于下面的代码
class Example {
private val delegate = Delegate()
var p: String
set(value: String) = delegate.setValue(...,value)
get() = delegate.getValue(...)
}
class Delegate {
operator fun getValue(...): String {...}
operator fun setValue(..., s: String) {...}
}
by 右边的表达式不一定是新创建的实例,也可以是函数调用、另 一个属性或任何其他表达式,只要这个表达式的值,是能够被编译器用正确的参数类型来调用getValue 和 setValue 的对象。与其他约定一样, getValue 和 setValue 可以是对象自己声明的方法或扩展函数。
总结
这篇文章主要介绍了Kotlin中的约定特性,Kotlin通过约定大大简化了常见方法的使用,同时提高了程序的可读性。