在 Kotlin 中,如果一个类定义了一个名为 plus 的函数,那就可以在该类的实例上使用 + 运算符。这种技术称为约定
。
下面看看 Kolint 中约定的使用。
重载算术运算符
重载二元算术运算符
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
val a = Point(1, 2) // Point(x=1, y=2)
val b = a + Point(3, 4) // Point(x=3, y=6)
// 相当于 val b = a.plus(Point(3, 4))
使用扩展函数也可以:
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 |
位运算
Kotlin 中没有位运算符,因此也无法自定义。Kotlin 中的位运算使用常规函数进行:
- shl——带符号左移
- shr——带符号右移
- ushr——无符号右移
- and——按位与
- or——按位或
- xor——按位异或
- inv——按位取反
重载复合赋值运算符
有时我们会用到 +=、-=、*= 这种复合赋值运算符。
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
var a = Point(1, 2)
a += Point(3, 4) // Point(x=4, y=6),一个新的 Point 对象
// 相当于 a = a + Point(3, 4)
当定义了 + 运算符后,就可以使用 +=,这时 a 会指向一个新的对象。所以 a 必须是 var 的。
有时我们操作 += 时,其实是改变 a 的内容,并不想换一个新的对象,那可以重载 plusAssign 方法(类似的有 minusAssign 等方法)。
operator fun Point.plusAssign(other: Point) {
this.x += other.x
this.y += other.y
}
val a = Point(1, 2)
a += Point(3, 4) // // Point(x=4, y=6),还是原来的 Point 对象
这时 a 的引用并没有变,所以它可以是 val 的。
可以看出,+= 运算符可能代表两种函数,一种是 plus,一种是 plusAssign,当两个函数都有定义时,可以通过 var、val 进行区分。但最好还是不要定义两个。
重载一元运算符
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
val a = Point(1, 2) // Point(x=1, y=2)
val b = -a // Point(x=-1, y=-2)
可重载的一元运算符:
表达式 | 函数名 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
–a, a– | dec |
重载比较表达式
equals
== 运算符,会被转换为 equals 的调用。
a == b
// 相当于
a?.equals(b) ?: (b == null)
重载 equals 方法:
class Point(var x: Int, var y: Int) {
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is Point) return false
return other.x == x && other.y == y
}
}
上面用到了 === 运算符,这和 Java 的 == 一致,表示两个参数是不是同一个对象。=== 运算符不能被重载。
equals 使用 override 关键字进行修饰,而不是其他运算符的 operator 关键字。因为 equals 在 Any 类中定义时已经带了 operator 关键字。
compareTo
>、>=、<、<= 运算符会被转换为 compareTo 的调用。
compareTo 是 Java 中 Comparable 接口对象比较的简明语法。
// java
a.compareTo(b) >= 0
// kotlin
a >= b
Kotlin 标准库中的 compareValuesBy 函数也提供了 compareTo 的简洁实现。
val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(compareValuesBy(p1, p2) {it.x})
集合和区间的约定
下标访问:get 和 set
[ ] 操作符会被转换为 get、set 的调用。
实现 get 方法:
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
val a = Point(1, 2)
a[0] // 1
实现 set 方法:
operator fun Point.set(index: Int, value: Int) {
when (index) {
0 -> this.x = value
1 -> this.y = value
else -> throw Exception("invalid index")
}
}
val a = Point(1, 2)
a[1] = 8
a // Point(x=1, y=2)
in 的约定
in 操作符会被转换为 contains 的调用。
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
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) // true
println(Point(1, 2) in rect) // false
rangeTo 的约定
… 运算符会被转换为 rangeTo 的调用。
val now = LocalDate.now()
val vacation = now..now.plusDays(7)
println(now.plusDays(2) in vacation) // true
now..now.plusDays(7)
会被转换为 now.rangeTo()
。rangeTo 并不是 LocalDate 的成员函数,而是 Comparable
的一个扩展函数。所以实现了 Comparable 接口的函数都可以直接使用 … 运算符。
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
在“for”循环中使用“iterator”的约定
在 for 循环中使用 in,会调用到 iterator 方法。如 for(x in list) {…} 会被转换为 list.iterator() 的调用。
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean = current <= endInclusive
override fun next(): LocalDate = current.apply {
current = plusDays(1)
}
}
val now = LocalDate.now()
val vacation = now..now.plusDays(2)
for (day in vacation) {
println(day)
}
// 输出
2018-10-29
2018-10-30
2018-10-31
解构声明和组件函数
Kotlin 支持像 python 一样,把参数分离出来:
val (name, age) = person
这种操作被称为解构声明,解构声明会被转换为 componentN 函数的调用。
使用前,用 operator 关键字来标识 componentN 方法(最多可以有5个):
class Pair<K, V, W>(val first: K, val second: V, val third: W) {
operator fun component1(): K = first
operator fun component2(): V = second
operator fun component3(): W = third
}
数据类默认声明了 componentN() 方法,可以直接使用。
解构声明和 map:
for ((key, value) in map) {
}
下划线标识用不到的变量:
val (_, status) = getResult()
lambda 解构:
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
两个参数和一个参数解构的区别:
{ a -> ... } // one parameter
{ a, b -> ... } // two parameters
{ (a, b) -> ... } // a destructured pair
{ (a, b), c -> ... } // a destructured pair and another parameter
可以声明整个解构参数的类型,也可以单独声明某个 component 的类型:
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }
map.mapValues { (_, value: String) -> "$value!" }
重用属性访问的逻辑:委托属性
class Example {
var p: String by Delegate()
}
通过 by 关键字,引入委托属性。p 属性的 get()、set() 方法会被委托给 Delegate 对象的 getValue()、setValue() 方法。
class Delegate {
operator fun getValue(thisRef: Any?, prop: KProperty<*>): String {
return "$thisRef, thank you for delegating '${prop.name}' to me!"
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
println("$value has been assigned to ${prop.name} in $thisRef")
}
}
执行get、set。
val example = Example()
println(example.p)
example.p = "hello"
// output
Example@179d3b25, thank you for delegating 'p' to me!
hello has been assigned to p in Example@179d3b25
thisRef — 被委托的类或它的超类
prop — KProperty<*> 或它的超类
库函数的委托方法
lazy
val q: String by lazy {
println("hello")
"world"
}
println(q)
println(q)
println(q)
// output
hello
world
world
world
lazy 提供一个拥有 getValue 方法的委托对象,只在被委托的对象第一次调用时初始化一次,之后都返回第一次初始化后的值。lazy 函数是线程安全的。
observable
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$prop, $old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
// output
var User.name: kotlin.String,<no name> -> first
var User.name: kotlin.String,first -> second
observable 可以添加属性改变的观察者。它接收两个参数,一个初始值,一个值变化的处理器,每次属性值变化后都会调用这个处理器。处理器提供三个参数,被改变的属性、属性的旧值、属性的新值。
把属性存入 map
Map、MutableMap 接口定义了 getValue 、setValue 函数,可以把属性委托给它们。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
println(user.name) // "John Doe"
println(user.age) // 25