1 重载算术运算符
Kotlin可以通过扩展函数的机制为现有的类增添新的方法。可以把任意约定方法定义为扩展函数,从而适应任何现有的Java类而不用修改其代码。
.1重载二元算术运算
定义一个 plus 运算符
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
fun main(args: Array) {
val p1 = Point(10, 20)
val p2 = Point(30, 40)
println(p1 + p2)
}
注意,如何使用 operator 关键字来声明 plus 函数。用于重载运算符的所有函数都需要用该关键字标记,用来表示你打算把这个函数作为相应的约定的实现,并且不是碰巧地定义一个同名函数。在使用了 operator 修饰符声明了 plus 函数之后,你就可以直接使用+号来求和了。事实上,这里它调用的是 plus 函数 。如下:
a + b ---> a.plus(b)
除了把这个运算符声明为一个成员函数外, 也可以把它定义为一个扩展函数。把运算符定义为扩展函数,如下:
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other .y)
}
这样实现是一样的。后续的示例中将会使用扩展函数的语法来写,这是给第三方库的类定义约定扩展函数的常用模式。而且 , 对于你自己的类这种语法也同样适用。
可重载的二元算术运算符:
a*b 函数名: times
a/b div
a%b mod
a+b plus
a-b minus
运算符 *,/和%具有相同的优先级,高于+和 - 运算符的优先级。
定义一个运算数类型不同的运算符
data class Point(val x: Int, val y: Int)
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
fun main(args: Array) {
val p = Point(10, 20)
println(p * 1.5)
}
注意 : Kotlin 运算符不会自动支持交换性 (交换运算符的左右两边)。如果希望用户能够使用 1.5 * p 以外, 还能使用 p * 1.5 ,你需要为它定义一个单独的运算符 : operator fun Double. times (p : Point): Point 。
2 重载复合赋值运算符
通常情况下,当你在定义像 plus 这样的运算符函数时, Kotlin 不止支持+ 号运算 ,也支持+= 。 像+=、-=等这些运算符被称为复合赋佳运算符。看这个例子:
>> var point= Point(l, 2)
>> point+= Point(3 , 4)
>> println(point)
Point(x=4 , y=6)
这等同于 point= point + Point( 3 , 的的写法。当然 ,这个只对于可变变量有效。
在一些情况下,定义+=运算可以修改使用它的变量所引用的对象,但不会重新分配引用 。将一个元素添加到可变集合,就是一个很好的例子 :
>> val numbers = ArrayList ()
>> numbers += 42
>> println(numbers[O])
42
如果你定义了一个返回值为 Unit,名为 plusAssign 的函数, Kotlin 将会在用到+=运算符的地方调用它。其他二元算术运算符也有命名相似的对应函数:如minusAssign 、 timesAssign 等 。
3 重载一元运算符
重载一元运算符的过程与你在前面看到的方式相同:用预先定义的一个名称来声明函数(成员函数或扩展函数),并用修饰符 operator 标记。我们来看一个例子 。
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
fun main(args: Array) {
val p = Point(10, 20)
println(-p)
}
4 重载比较运算符
如果在 Kotiin 中使用==运算符,它将被转换成 equals 方法的调用。这只是我们要讨论的约定原则中的一个。
使用 ! =运算符也会被转换成 equals 函数的调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==和!=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为 null 。比较 a == b 会检查 a 是否为非空,如果不是,就调用 a . equals (b ) (如图 7.4 所示);否则,只有两个参数都是空引用,结果才是 true 。
a == b --> a?.equals(b) ?: (b == null)
等式校验 == 被转换为 equals 函数的调用 , 以及 null 的校验
5 集合与区间的约定
处理集合最常见的一些操作是通过下标来获取和设置元素,以及检查元素是否属于当前集合。所有的这些操作都支持运算符语法:要通过下标获取或设置元素 ,可以使用语法 a [b] (称为下标运算符) 。 可以使用 in 运算符来检查元素是否在集合或区间内,也可以迭代集合。可以作为集合的自定义类。让我们来看看用于支持这些操作的约定。
5.1 通过下标来访问元素:“get”和“set”
在 Kotiin 中,可以用类似 Java 中数组的方式来访问 map 中的元素一一使用方括号:
val value = map [key]
也可以用同样的运算符来改变一个可变 map 的元素:
mutableMap[key) = newValue
来看看它是如何工作的。在 Kotlin 中,下标运算符是一个约定。使用下标运算符读取元素会被转换为 get 运算符方法的调用 ,井且写入元素将调用 set 。 Map 和MutableMap 的接口己经定义了这些方法。让我们看看如何给自定义的类添加类似的方法。
data class Point(val x: Int, val y: Int)
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
fun main(args: Array) {
val p = Point(10, 20)
println(p[1])
}
输出: 20
你只需要定义一个名为 get 的函数,并标记 operator。之后,像 p [1]这样的表达式,其中 p 具有类型 Point ,将被转换为 get 方法的调用,
x[a.b] --> x.get(a,b)
方括号的访问会被转换为get函数的调用注意, get 的参数可以是任何类型,而不只是 Int 。例如,当你对 map 使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义具有多个参数的 get 方法。例如,如果要实现一个类来表示二维数组或矩阵,你可以定义一个方法,例如 operator fu口 get (rowindex: Int, colindex: Int ),然后用matrix [row,col ]来调用。如果需要使用不同的键类型访问集合,也可以使用不同的参数类型定义多个重载的 get 方法。
我们也可以用类似的方式定义一个函数,这样就可以使用方括号语法更改给定下标处的值。 Point 类是不可变的,所以定义 Poin t 的这种方法是没有意义的 。作为例子,我们来定义另一个类来表示一个可变的点。
data class MutablePoint(var x: Int, var y: Int)
operator fun MutablePoint.set(index: Int, value: Int) {
when(index) {
0 -> x = value
1 -> y = value
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
fun main(args: Array) {
val p = MutablePoint(10, 20)
p[1] = 42
println(p)
}
输出:
MutablePoint(x=lO, y=42)
6 “in”的约定
集合支持的另一个运算符是川运算符,用于检查某个对象是否属于集合 。 相应的函数叫作 contains 。我们来实现一下,使用 in 运算符来检查点是否属于一个矩形 。栗子:
data class Point(val x: Int, val y: Int)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
}
fun main(args: Array) {
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
println(Point(5, 5) in rect)
}
分析:in 右边的对象将会调用 cont a ins 函数, in 左边的对象将会作为函数入参 。在 Rectangle.contains 的实现中,我们用到了的标准库的 until 函数,来构建一个开区间,然后使用运算符 in 来检查某个点是否属于这个区间。
a in c ---> c.contains(a) in 操作将会转换为 contains 函数的调用
7 rangeTo 的约定
要创建一个区间,请使用 ..语法:举个例子, 1. . 10 代表所有从 1 到 10 的数字。在前面你己经看到过区间的使用,现在让我们来研究一下创建它的约定。 .. 运算符是调用 range To 函数的一个简洁方法
start..end ---> start.rangeTo(end) ..运算符将被转换为 rangeTo 函数的调用
range To 函数返回一个区间。你可以为自己的类定义这个运算符。但是,如果该类实现了 Comparable 接口,那么不需要了 : 你可以通过 Kotiin 标准库创建一个任意可比较元素的区间,这个库定义了可 以用 于任何可比较元素的 range To 函数:
operator fun > T.rangeTo(that: T): ClosedRange
这个函数返回一个区间,可以用来检测其他一些元素是否属于它。
8 解构声明和组件函数
解构声明。这个功能允许你展开单个复合值,并使用它来初始化多个单独的变量。
val p = Point(10,20)
val(x,y) = p
println(x)
10
println(y)
20
一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。事实上,解构声明再次用到了约定的原理。要在解构声明中初始化每个变量,将调用名为 componentN 的函数,其中 N 是声明中变量的位置。换句话说如下:
-- val a = p.component1()
val(a,b) = p --- |
-- val b = p.component2()
分析: 解构声明被转换为 componentN函数的调用,对于数据类,编译器为每个在主构造方法中声明的属性生成一个 componentN函数。下面的例子显示了如何手动为非数据类声明这些功能 :
class Point(val x: Int, val y :Int) {
operator fun cornponentl() = x
operator fun cornponent2() = y
}
解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。如果要这样做,可以定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。在调用函数后,可以用解构声明的方式,来轻松地展开它,使用其中的值。举个例子,让我们编写一个简单的函数,来将一个文件名分割成名字和扩展名。
data class NameComponents(
val name: String,
val extension: String)
fun splitFilename(fullName: String): NameComponents {
val (name, extension) = fullName.split('.', limit = 2)
return NameComponents(name, extension)
}
输出:
example
kt
解构声明和循环
解构声明不仅可以用作函数中的顶层语句,还可以用在其他可以声明变量的地方,例如 in 循环。一个很好的例子,是枚举 map 中的条目 。 下面是一个小例子,
fun printEntries(map: Map) {
for ((key, value) in map) {
println("$key -> $value")
}
}
fun main(args: Array) {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
}
这个简单的例子用到了两个 Kotlin 约定 : 一个是迭代一个对象,另 一个是用于解构声明。 Kotiin 标准库给 m叩增加了一个扩展的 iterator 函数,用来返回map条目的法代器。因此,与 Java 不同的是,可以直接送代 map。它还包含 Map.Entry 上的扩展函数 componentl 和 component2 ,分别返回它的键和值。实际上,前面的循环被转换成了这样的代码:
for (entry in map.entries) {
val key = entry. cornponentl ()
val value= entry.cornponent2()
//. . .
}