运算符重载及其他约定

Kotlin约定

Java在标准库中有一些与特定的类相关联的语言特性。例如,实现了java.lang.Iterable接口的对象可以在for循环中使用。

在Kotlin中,也有类似的约定,但这些功能与特定的函数命名相关,而不是与特定的类型绑定。例如,在类中定义一个名为plus的特殊方法,那么按照约定,就可以在该类的实例上使用+运算符。

运算符重载

用于重载运算符的所有函数都需要用operator关键字来标记,用来表示你打算把这个函数作为相应的约定的实现。

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}
1
2
3
4
5

运算符的运算会自动转换成对应函数的调用。例如,point1 + point2会自动转换成point1.plus(point2)

除了把这个运算符声明为一个成员函数外,也可以把它定义为一个扩展函数。

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}
1
2
3

Kotlin限定了你能重载哪些运算符,以及你需要在你的类中定义的对应名字的函数。

自定义类型的运算符,基本上和标准数字类型的运算符有着相同的优先级。

定义一个运算符时,不要求两个运算符是相同的类型。

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (xy* scale).toInt())
}
1
2
3

Kotlin运算符不会自动支持交换性。例如,point * 1.5是正确的,而1.5 * point是无效的。除非定义一个operator fun Double.plus(p: Point): Point

运算符函数的返回类型可以不同于任一运算数类型。

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}
1
2
3

和普通函数一样,可以重载operator函数。

重载算术运算符

重载二元算术运算

可重载的二元算术运算符。

表达式 函数名
a * b times
a / b div
a % b mod
a + b plus
a - b minus

Kotlin没有为标准数字类型定义任何位运算符,也不允许你来定义它们。以下为Kotlin提供的用于执行位运算的完整函数列表(只用于Int和Long):

表达式 含义
a shl 2 带符号左移
a shr 2 带符号右移
a ushr 2 无符号右移
a and b 按位与
a or b 按位或
a xor b 按位异或
a inv b 按位取反

重载复合赋值运算符

Kotlin不止支持+等运算符,也支持+=等复合赋值运算符。

+=运算符对应的函数名为plusAssign,其他二元算术运算符也有命名相似的对应函数,如minusAssign

注意,当使用+=时,如果同时定义了plusplusAssign且它们都适用,编译器会报错。但Kotlin标准库支持集合类同时定义这两种函数: +总是返回一个新的集合。 +=用于可变集合时,始终在一个地方修改原集合。而+=用于只读集合时,会返回一个修改过的副本。

重载一元运算符

可重载的一元运算符。

表达式 函数名
+a unaryPlus
-a unaryMinus
!a not
++aa++ inc
--aa-- dec

重载比较运算符

等号运算符

使用==!=运算符会被转换成equals函数的调用,且它们可用于可空运算数。

使用a == b相当于执行a?.equals(b) ?: (b == null)

不同于其他约定,因为equals函数是在Any类中定义的,所以你重新定义该函数时要使用override来标记。

class Point(val x: Int, val y: Int) {
    override fun equals(obj: Any?): Boolean {
        // ===为恒等运算符,此处用它来优化。另外,此运算符不可被重载
        if (obj === this) return true
        if (obj !is Point) return false
        return obj.x == x && obj.y == y
    }
}
1
2
3
4
5
6
7
8

另外,equals不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。

排序运算符

Kotlin支持Comparable接口,其compareTo方法可以按约定调用,即比较运算符(>, <, <=, >=)的使用将会转换成compareTo的调用。

a >= b相当于a.compareTo(b) >= 0

重写compareTo时,可在其中使用Kotlin标准库中的compareValueBy函数来简洁地实现。

class Person(val firstName: String, val lastName: String): Comparable<Person> {
    override fun compareTo(other: Person): Int {
        // 比较this和other,先比较它们的lastName,后比较它们的firstName
        return compareValueBy(this, other, Person::lastName, Person::firstName)
    }
}
1
2
3
4
5
6

compareValueBy函数接收用来计算比较值的一系列回调,按顺序依次调用回调方法,如果值不同,则返回比较结果;如果相同,则继续调用下一个;如果没有更多回调,则返回0。这些回调可以是lambda或属性等。

集合与区间的约定

下标约定

使用下标运算符读取元素会被转换为get方法的调用,并且写入元素将调用set方法。

`x[a]`相当于`x.get(a)`。

`x[a] = b`相当于`x.set(a, b)`。
operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}
1
2
3
4
5
6
7

键参数的类型可以是任何类型,而不只是Int。

in约定

集合中的in运算符,用于检查某个对象是否属于集合,实质上是调用了集合的contains函数。例如,a in c相当于c.contains(a)

判断a是否在[1, 10)之间,可使用a in 1 until 10(其中until为Kotlin标准库中的函数,用于创建一个开区间)。

rangTo约定

可使用..来创建一个闭区间,实质上是调用rangTo函数。例如,start..end相当于start.rangTo(end)

for循环中的iterator约定

for (a in b) {...}将被转换成b.iterator(),然后重复调用其hasNext()next()

解构声明和组件函数

解构声明允许你展开单个复合值,并使用它来初始化多个单独的变量。

val p = Point(10, 20)
val (x, y) = p
1
2

要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N为声明中变量的位置。例如,val (x, y) = p相当于执行如下操作:

val a = p.component1()
val b = p.component2()
1
2

编译器会为data class的主构造方法中声明的属性生成一个componentN的函数。

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}
1
2
3
4

标准库只允许此语法来访问一个对象的前5个元素。

用解构声明来遍历map。

for ((key, value) in map) {
    println("$key -> $value")
}
1
2
3
for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
}
1
2
3
4

委托属性

委托属性的基本操作

委托属性的基本语法如下,属性p将它的访问器逻辑委托给了Delegate类的一个实例对象。

class Foo {
    var p: Type by Delegate()
}
1
2
3

关键字by对它后面的表达式求值来获得属性的委托对象。

Delegate类必须具有getValuesetValue方法(后者仅适用于可变属性)。这两个方法可以是成员函数,也可以是扩展函数。

class Delegate {
    operator fun getValue(...) {...}
    
    operator fun setValue(..., value: Type) {...}
}
1
2
3
4
5

惰性初始化和by lazy()

惰性初始化是一种常见的模式,直到在第一次访问该属性时,才根据需要创建对象的一部分。

例如,在首次访问一个人的邮件列表时,才加载他的邮件列表。

class Person(val name: String) {
    private var _emails: List<Email>? = null
    
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this)
            }
            return _emails!!
        }
}
1
2
3
4
5
6
7
8
9
10
11

上面的解决方案,代码啰嗦,而且不是线程安全的。可利用Kotlin的lazy函数加上关键字by来创建委托属性。

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}
1
2
3

lazy函数的参数为一个lambda,并返回一个具有getValue方法的对象。之后把这个对象与by关键字一起创建一个委托属性。默认情况下,lazy函数是线程安全的。

委托属性的变换规则

假设有一个具有委托属性的类。

class C {
    var prop: Type by MyDelegate()
}
1
2
3

关键字by后接的表达式求值后得到的对象会被保存在一个名为<delegate>的隐藏属性中。编译器也将用一个KProperty类型的对象来代表这个属性,它被称为<property>

编译器生成的代码如下:

class C {
    private val <delegate> = MyDelegate()
    
    var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <property>.setValue(this, <property>, value)
}
1
2
3
4
5
6
7

val x = c.prop相当于val x = <delegate>.getValue(c, <property>)

c.prop = x相当于<property>.setValue(c, <property>, x)

map与委托属性

Kotlin标准库在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,于是,委托属性可以使用任意map来作为属性委托,来灵活地处理具有可变属性集的对象。

class Person {
    private val _attributes = hashMapOf<String, String>()
    
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    
    val name: String by _attributes
}
1
2
3
4
5
6
7
8
9

委托属性的名称将自动用作在map中的键,属性值作为map中的值。

  • person.name相当于_attributes["name"]

  • person.name = "daking"相当于_attributes["name"] = "daking"