Operator overloading and other conventions

主要内容

  1. 操作符重载
  2. 指名(special-named)函数:convension的一种实现
  3. 属性委派

类似于Java语言有好几样特性一样,譬如对象实现了java.lang.Iterable接口的可以使用for循环,实现了java.lang.AutoCloseable接口的可以使用try-with-resources语句(Java 8 之后)。Kotlin提供了一种 convertions 技术,实现对操作符的重载。

Overloading arithmetic operators

Kotlin中最直接的公约是算术运算符。在Java中,算术运算符仅可以用于原生类型,额外地+号可以用于连结String类型。但对于其它类如BigInteger,使用+比起使用add方法会更加直观。

Overloading binary arithmetic operations

Kotlin的公约提供了操作符重载,

1
2
3
4
5
6
7
8
9
10
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point { // Defines an operator function named "plus"
return Point(x + other.x, y + other.y)
}
}

>>> val p1 = Point(10, 20)
>>> val p2 = Point(30, 40)
>>> println(p1 + p2) // Calls the "plus" function using the + sign
Point(x=40, y=60)

这里用到operator关键字来声明plus函数。所有需要重载运算符都需要使用该关键字。表示显式地实现相应的约定。

Figure 7.1

另外一种写法是结合扩展函数定义,因为很多情况下你并不希望或不能修改原有的类。

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

下标罗列的约定的所有算术符号对应的函数名,

Expression Function name
a * b times
a / b div
a % b mod
a + b plus
a - b minus

运算符的定义并没有严格要求两个对象的类型必须一致,

1
2
3
4
5
6
7
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}

>>> val p = Point(10, 20)
>>> println(p * 1.5)
Point(x=15, y=30)

但是Kotlin的operator不支持交换律 commutativity 。所以你需要重新再定义以满足交换律。operator fun Double.times(p: Point): Point

返回类型也可以自定义,

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

>>> println('a' * 3)
aaa

** No special operators for bitwise operations**

Kotlin并没有定义位运算符。相反,位运算被定义为常规的中缀调用语法(infix call syntax)。如,

  • shl,左移
  • shr,右移
  • ushr,无符号右移
  • and,按位与
  • or,按位或
  • xor,按位异或
  • inv,按位反

譬如,

1
2
3
4
5
6
>>> println(0x0F and 0xF0)
0
>>> println(0x0F or 0xF0)
255
>>> println(0x1 shl 4)
16

Overloading compound assignment operators

复合赋值操作符( compound assignment operator ),跟算术操作符一样,

1
2
3
4
>>> var point = Point(1, 2)
>>> point += Point(3, 4)
>>> println(point)
Point(x=4, y=6)

对于可变集合来说,+= 操作相当于添加新的元素,

1
2
3
4
>>> val numbers = ArrayList<int>()
>>> numbers += 42
>>> println(numbers[0])
42

对于可变集合,Kotlin标准库中定义了plusAssign函数公约,

1
2
3
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}

代码中如果用到+=,理论上会调用到plusplusAssign函数,

Figure 7.2

如果定义的类是不可变的,相应定义返回新值的函数plus;如果是可变的,则定义返回可变实例的plusAssign函数。但请尽量不要同时都定义这两个函数。

而操作符+-的集合运算总是返回新的集合,其中,对于可变集合,直接修改其值;对于不可变集合,返回新的集合copy实例。

1
2
3
4
5
6
7
>>> val list = arrayListOf(1, 2)
>>> list += 3 // += changes "list".
>>> val newList = list + listOf(4, 5) // + returns a new list containing all the elements.
>>> println(list)
[1, 2, 3]
>>> println(newList)
[1, 2, 3, 4, 5]

Overloading unary operators

Kotlin中支持一元操作( unary )。

1
2
3
4
5
6
7
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}

>>> val p = Point(10, 20)
>>> println(-p)
Point(x=-10, y=-20)

一元操作函数重载不需要任何参数,

Figure 7.3

列表如下,

Expression Function name
+a unaryPlus
-a unaryMinus
!a not
++a a++ inc
--a a-- dec

对于自增inc或自减dec函数来说,编译器会自动支持前置或后置的语义识别,

1
2
3
4
5
6
7
operator fun BigDecimal.inc() = this + BigDecimal.ONE

>>> var bd = BigDecimal.ZERO
>>> println(bd++) // Increments after the first println statement executes
0
>>> println(++bd) // Increments before the second println statement executes
2

Overloading comparison operators

Kotlin约定函数中也相应定义了比较操作符(==!=><等)。

Equality operators: “equals”

== 操作在Kotlin中真正被调用为equals方法。!=实际上也是被翻译为equlas的调用,只不过为相反方式。不同的是==!=可以用于对null的比较,因为它底层可以检测null值。

Figure 7.4

Ordering operators: compareTo

compareTo方法被定义在Comparable接口中。但是Java中只有原生类型采用使用比较操作符><;其它类型只能显式地使用element1.compareTo(element2)来比较两个对象。

Kotlin支持同样的Comparable接口。但是compareTo方法定义的接口在Kotlin中无法进行重载,因为它底层被翻译为compareTo的调用,

Figure 7.5

equlas一样,你需要继承Comparable接口对其方法进行重载,

1
2
3
4
5
6
7
8
9
10
class Person(val firstName: String, val lastName: String): Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName, Person::firstName) // Evaluates the given callbacks in order, and compares values
}
}

>>> val p1 = Person("Alice", "Smith")
>>> val p2 = Person("Bob", "Johnson")
>>> println(p1 < p2)
false

compareValuesBy为Kotlin的内联函数,该函数接收一系列回调并比较返回值。

Conventions used for collections and ranges

Accessing elements by index: “get” and “set”

Kotlin约定支持 index operator 下标操作符,语法和Java的数组访问类似,

1
val value = map[key]

你也可以直接对其进行赋值操作,

1
mutableMap[key] = newValue

Kotlin中的下标操作符,对于读取调用了get方法,对于写操作调用了set。该方法在MapMutableMap接口早已定义。你可以在自定义类中实现,

1
2
3
4
5
6
7
8
9
10
11
operator fun Point.get(index: Int): Int {  // Defines an operator function named "get"
return when(index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}

>>> val p = Point(10, 20)
>>> println(p[1])
20

当调用p[1]时,将被翻译为get方法的调用。

需要注意的是,get方法的参数可以任意,例如你可以实现一个二维数组的下标读operator fun get(rowIndex: Int, colIndex: Int),然后通过matrix[row, col]

Figure 7.6

类似地,下标的写操作要求对象可变的,因此你需要声明可变的对象属性,否则更改是无意义的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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")
}
}

>>> val p = MutablePoint(10, 20)
>>> p[1] = 42
>>> println(p)
MutablePoint(x=10, y=42)

Figure 7.7

The “in” convention

Kotlin集合中另一个支持的操作符为in,用于检测对象是否被包含在集合中。对应被调用的函数是contains

1
2
3
4
5
6
7
8
9
10
11
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 // Builds range, and checks that coordinate "x" belongs to this range
}

>>> val rect = Rectangle(Point(10, 20), Point(50, 50))
>>> println(Point(20, 30) in rect)
true
>>> println(Point(5, 5) in rect)
false

Figure 7.8

The rangeTo convention

..操作符对应于rangeTo函数的调用,用于常见Range对象的实例,

Figure 7.9

你可以为自定义类扩展该公约。但如果你的函数继承了Comparable,则不需要了:因为标准库为任意的Comparable实例定义了Range的方法。标准库中定义的rangeTo函数带有泛型参数,

1
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

我们以LocalDate类为例,

1
2
3
4
>>> val now = LocalDate.now()
>>> val vacation = now..now.plusDays(10) // Creates a 10-day range starting from now
>>> println(now.plusWeeks(1) in vacation) // Checks whether a specific date belongs to range
true

表达式now..now.plusDays(10)被编译器翻译为now.rangeTo(now.plusDays(10))rangeTo函数并不是LocalDate的成员,但是Comparable的扩展函数。

在算术操作符中,rangeTo操作符的优先级最低,所以最好用括号括起来以避免歧义:

1
2
3
>>> val n = 9
>>> println(0 ..(n + 1)) // You can write 0..n + 1, but parentheses make it clearer.
0..10

注意表达式0..n.forEach {}不会被编译成功,因为你必须用括号括起来才能作为表达式:

1
2
>>> (0..n).forEach { print(it) }  // Put a range in parentheses to call a method on it.
0123456789

The “iterator” convention for the “for” loop

迭代表达式for (x in list) { ... }编译时被翻译为list.iterator(),和Java一样,不停地重复调用hasNextnext方法。

在Kotlin中它被作为convention约定,意味着iterator方法可以扩展定义。其中,标准库中就有对字符串的iterator的扩展定义,

1
2
3
operator fun CharSequence.iterator(): CharIterator

>>> for (c in "abc") {}

你可以在自己的类定义iterator方法,例如,

1
2
3
4
5
6
7
8
9
10
11
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 newYear = LocalDate.ofYearDay(2017, 1)
>>> val daysOff = newYear.munusDays(1)..newYear
>>> for (dayOff in daysOff) { println(dayOff) }
2016-12-31
2017-01-01

Destructuring declarations and component functions

destructuring declaractions ,Kotlin的声明解构作为feature实现。

1
2
3
4
5
6
>>> val p = Point(10, 20)
>>> val (x, y) = p // Declares variables x and y, initialized with components of p
>>> println(x)
10
>>> println(y)
20

声明解构在Kotlin中也是作为convension约定,它的底层原理如下,

Figure 7.10

对于data class而言,编译器会为每个属性在primary constructor声明的属性创建相应的componentN函数。

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

声明解构的一个好处在于可以从一个函数中返回多个值。

1
2
3
4
5
6
7
8
9
10
11
12
data class NameComponents(val name: String, val extension: String) // Declares a data class to hold the values

fun splitFilename(fullName: String): NameCompoents {
val result = fullName.split('.', limit = 2)
return NameComponents(result[0], result[1]) // Returns an instance of the data class from the function
}

>>> val (name, ext) = splitFilename("example.kt") // Uses the destructuring declaration syntax to unpack the class
>>> println(name)
example
>>> println(ext)
kt

Destructuring declarations and loops

声明解构常常用于map的迭代,

1
2
3
4
5
6
7
8
9
10
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value") // Destructuring declaration in a loop
}
}

>>> val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>> printEntries(map)
Oracle -> Java
JetBrains -> Kotlin

Resuing property accessor logic: delegated properties

delegated properties仅属于Kotlin中的独特的强大的特性。该特性让你以最简单的方式实现属性,让其原本以更复杂的方式存储,但不会重复实现逻辑。例如,属性可以存储在数据库表、浏览器session、map中等。

该特性的基础是委派( delegation ):一种设计模式。

Delegated properties: the basics

常规的委派属性语法如下:

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

语法格式为 val /var <property name>: <Type> by <expression>by之后的表达式是一个委派,因为属性对应的get()set()会通过约定委派给它的getValue()setValue()方法。属性委派不需要实现接口,但需要提供getValue()函数(和setValue() 如果是var可变属性)。

例如,

1
2
3
4
5
class Foo {
private val delegate = Delegate()
var p: Type // Generated accessors of the "p" property call the getValue and setValue methods on "delegate".
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)

按照约定,Delegate类必须要有getValuesetValue方法(setValue仅为可变属性时被要求)。通常它们可以是成员或扩展。为了简化解析,我们省略它们的参数,Delegate类看起来会像,

1
2
3
4
5
6
7
8
9
10
11
12
class Delegate {
operator fun getValue(...) { ... } // The getValue method contains the logic for implementing a getter.
operator fun setValue(..., value: Type) { ... } // The setValue method contains the logic for implementing a setter.
}

class Foo {
var p: Type by Delegate() // The "by" keyword asociates a property with a delegate object.
}

>>> val foo = Foo()
>>> val oldValue = foo.p // Accessing a property foo.p calls delegate.getValue(...) under the hood.
>>> foo.p = newValue // Changing a property value calls delegate.setValue(..., newValue).

foo.p被作为常规属性使用,但底层调用了帮助类Delegate的方法调用。Kotlin标准库针对不同的委派模式提供了工厂方法调用。官方之为Standard delegates,包含两部分,lazy properties和observable properties。

Using delegated properties: lazy initialization and “by lazy()”

Lazy initialization 仅在对象第一次访问时才被初始化。

例如,假设有一个类Person允许你访问他的邮箱列表。其中邮箱被存储在数据库中需要花费一段时间才能访问。你希望数据库的访问操作仅一次也第一次属性访问时才触发。

1
2
3
4
5
class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
println("Load emails for ${person.name}"}
return listOf(/*...*/)
}

下面通过额外属性_emails存储null方式实现lazy加载的目的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person(val name: String) {
private var _emails List<Email>? = null // "_emails" property that stores the data and to which "emails" delegates

val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this) // Loads the data no access
}
return _emails!! // If the data was loaded before, returns it
}
}

>>> val p = Person("Alice")
>>> p.emails // Emails are loaded on first access.
Load emails for Alice
>>> p.emails

但这种写法有点啰嗦,如果有好几个lazy properties需要实现会显得繁琐。另外,上述代码也不是线程安全的。

Kotlin标准库提供了by lazy的函数调用,

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

lazy函数返回包含getValue方法签名的一个对象,因此你可以结合by关键一起创建委派属性。这里的lazy的入参是一个lambda用于初始化。lazy函数默认是线程安全的。

Implementing delegated properties

下面以示例方式,阐述委派属性是如何实现的。我们希望实现最常见的事件通知类,

1
2
3
4
5
6
7
8
9
10
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}

fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}

编写Person类,希望属性在发生变更时触发listener,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() {
var age: Int = age
set(newValue) {
val oldValue = field // The "field" identifier lets you access the property backing field.
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue) // Notifies listeners about the property change
}

var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary", oldValue, newValue)
}
}

>>> val p = Person("Dmitry", 34, 2000)
>>> p.addPropertyChangeListener(
... PropertyChangeListener { event -> // Attaches a property change listener
... println("Property ${event.propertyName} changed " + "from ${event.oldValue} to ${event.newValue}")
... }
... )
>>> p.age = 35
Property age changed from 34 to 35
>>> p.salary = 2100
Property salary changed from 2000 to 2100

这里的setter部分代码有许多重复的地方。我们通过委派方式将其分离出单独一个类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ObservableProperty(val propName: String, var propValue: Int, val changeSupport: PropertyChangeSupport) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(value) { _age.setValue(value) }
val _salary = ObservableProperty("salary", salary, changeSupport)
var salary: Int
get() = _salary.getValue()
set(value) { _salary.setValue(value) }
}

现在已经接近理解kotlin的委派属性是如何工作的了。但仍然有少许的样板代码,kotlin的委派属性的特性让你可以移除掉这部分样板代码(boilerplate)。在此之前,你需要按照约定,更改ObservableProperty相关方法的签名。

1
2
3
4
5
6
7
8
class ObservableProperty(var porpValue: Int, val changeSupport: PropertyChangeSupport) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}

对比上一个版本,有如下改变,

  • getValuesetValue函数标记有operator,按照约定要求。
  • 这些函数有两个参数:一个是该属性的实例对象,另一个是属性自身。该属性用KProperty的一个对象表示。你可以通过KProperty.name访问属性名。
  • name属性从第一构造函数(primary constructor)移除掉了,因为你可以从KProperty直接访问。

最终的委派属性的使用将变得非常简短,

1
2
3
4
class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}

通过by关键字,kotlin编译器自动地做了前面版本本应手动要做的部分。对比前面的Person类:编译器生成的代码非常相似。by右边的对象被称为 delegate 。kotlin将委派存储在一个隐藏属性,通过委派上的getValuesetValue的调用来修改或访问原来的属性。

如其手写这个ObservableProperty类,不妨使用Kotlin的标准库,

1
2
3
4
5
6
7
8
9
class Person (val name: String, age: Int, salary: Int): PropertyChangeAware() {
private val observer = {
prop: KProperty<*>, oldValue: Int, newValue: Int ->
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}

var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}

by右边的表达式不需要创建新的实例。可以是一个函数调用。只要该表达式的值对象是能被编译器调用getValuesetValue即可。

Delegated-property translation rules

让我们总结下委派属性是如何工作的。假设你的类包含如下委派属性:

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

val c = C()

MyDelegate()的实例将被存储在一个隐藏属性,我们将其作为 <delegate>引用。编译器将使用一个KProperty类型的对象来表示该属性。我们将其作为 <property>引用。

编译器会生成如下代码,

1
2
3
4
5
6
class C {
private val <delegate> = MyDelegate()

var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)

因此,每个属性访问器(property accessor)内部,编译器生成相应的getValuesetValue方法进行调用,结构如下,

Figure 7.11

Storing property values in a map

委派属性带来的另外一个常见模式是,对象拥有了动态定义的属性。这类对象有时被称为 expando objects 。例如,假设有一个联系人管理系统存储了联系人的任意信息。系统中的每个人仅包含一少部分属性,也包含有一少部分额外的属性(attribute)(例如,孩提期生日)。

实现该系统的一种方式是将联系人的所有attribute都存储在一个map,并提供访问该信息的对应属性(property)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!! // Retrieves the attribute from the map manually
}

>>> val p = Person()
>>> val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
>>> for ((attrName, value) in data)
... p.setAttribute(attrName, value)
>>> println(p.name)
Dmitry

简化代码的实现,使用by关键字委派属性,

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

val name: String by _attributes
}

这里是可以工作的,因为标准库为MapMutableMap接口提供了getValuesetValue的扩展函数。

Delegated properties in frameworks

对于一个object内部的property修改也极其简单。假设数据库表Users包含两列:字符串类型name、整型age。Kotlin
的定义如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable

object Users : IntIdTable() { // The object corresponds to a table in the database.
val name = varchar("name", length = 50).index() // Properties correspond to columns in this table.
val age = integer("age")
}

class User(id: EntityID<Int>) : IntEntity(id) { // Each instance of User corresponds to a specific entity in the table.
var name: String by Users.name // The value of "name" is the value stored in the database for that user.
var age: Int by Users.age
}

框架exposed对于实体类属性(attribute)实现了委派属性,使用了列对象(Users.name, Users.age)进行委派:

1
2
3
4
class User(id: EntityID): IntEntity(id) {
var name: String by Users.name // Users.name is a delegate for the "name" property.
var age: Int by Users.age
}

而对于现实指定的数据库列对象,

1
2
3
4
object Users : IntIdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}

框架底层定义了convention进行委派,

1
2
3
4
5
6
7
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// retrieve the value from the database
}

operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
// update the value in the database
}

你可以使用Column属性(Users.name)作为被委派的属性(name)。

Summary

  • Kotlin的约定(convention)定义了一些标准函数,可以让你对其进行重载以实现自定义类的operations。
  • comparison operator对应会调用到equalscompareTo方法。
  • 通过定义getsetcontains函数,你可以在自定义的类进行类似于集合的[]in的操作。
  • Kotlin的约定(convention)也支持集合Range的迭代语法。
  • 声明解构(destructuring declarations)可以让你初始化多个值,它的底层也是一种约定,对应调用了代码编译器生成的componentN函数。
  • 委派属性(delegated property)允许属性部分逻辑的重用,譬如存储、初始化、访问、修改等。
  • lazy标准库函数提供了属性的lazily initialized。
  • Delegates.observalbe函数定义在标准库中,实现了观察者模式。