Kotlin教程(七)运算符重载及其余约定

写在开头:本人打算开始写一个Kotlin系列的教程,一是使本身记忆和理解的更加深入,二是能够分享给一样想学习Kotlin的同窗。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展现出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。java

Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其余约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型算法


如你所知,Java在标准库中有一些与特定的类相关联的语言特性。例如,实现了java.lang.Iterable接口的独享能够在for循环中使用,实现了java.lang.AutoCloseable接口的对象能够在try-with-resources语句中使用。
Kotlin也有许多特性的原理很是相似,经过调用本身代码中定义的函数,来实现特定语言结构。可是,在Kotlin中,这些功能与特定的函数命名相关,而不是与特定的类型绑定。数据库

这一章咱们会用到一个UI框架中常见的类Point来演示,来看下定义:编程

data class Ponit(val x: Int, val y: Int)
复制代码

重载算术运算符

在Java中,全套的算数运算只能用于基本数据类型,+运算符能够与String值一块儿使用。可是,这些运算符在其余一些状况下用起来也很方便。例如,在使用哪一个BigInteger类处理数字的时候,使用+号就比掉用add方法显得更为优雅:给集合添加元素的时候,你可能也在想要是能用+=运算符就行了,在Kotlin中,你就能够这样作。数组

重载二元算术运算

咱们来支持第一个运算,把两个点加到一块儿:安全

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}
>>> val p1 = Point(10, 20)
>>> val p2 = Point(30, 40)
>>> println(p1 + p2)
Point(x=40, y=60)
复制代码

用于重载运算符的全部函数都须要使用operator关键字标记,表示你把这个函数做为相应的约定的实现,而且不是碰巧地定义了同名函数。
使用operator修饰符声明plus函数以后,你就能够直接使用+号来求和了。实际上调用的时plus函数a + b -> a.plus(b)。 除了声明成为一个成员函数外,也能够定义为一个扩展函数,一样有效:bash

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}
复制代码

Kotlin中可重载的二元算术运算符多线程

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

自定义类型的运算符,基本上和与标准数字类型的运算符有着相同的优先级。例如a + b * c,乘法将之中在加号以前执行。运算符*/%具备相同的优先级,高于+-运算符的优先级。并发

运算符函数和Java框架

从Java调用Kotlin运算符很是容易:由于每一个重载的运算符都被定义为一个函数,能够像普通函数那样调用它们。当从Kotlin调用Java的时候,只要Java代码中存在函数名和参数数量都匹配的函数,就能够在Kotlin中使用。若是Java已经存在相似的方法,可是方法名不一样,能够经过扩展函数来修正这个函数名,用来代替现有的Java方法。

当你定义一个运算符的时候,不要求两个运算数是相同的类型,例如,让咱们定义一个运算符,它容许你用一个数字来缩放一个点,能够用它在不一样坐标系之间作转换:

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}
>>> val p1 = Point(10, 20)
>>> println(p1 * 1.5)
Point(x=15, y=30)
复制代码

注意,Kotlin运算符不会自动支持交换性(交换运算符的左右两边)。若是但愿用户可以使用p * 1.5之外,还能使用1.5 * p,你须要为它定义一个单独的运算符operator fun Double.times(p: Point) : Point

运算符函数的返回类型能够不一样于任一运算数类型,例如,能够定义一个运算符,经过屡次重复单个字符来建立字符串:

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}
>>> println('a' * 3)
aaa
复制代码

这个运算符接收一个Char做为左值,Int做为右值,而后返回一个String类型。

和普通的函数同样,能够重载operator函数:能够定义多个同名的,但参数类型不一样的方法。

没有用于位运算的特殊运算符

Kotlin没有为标准数字类型定义任何位运算符。所以,也不容许你为自定义类型定义它们,相反,它使用支持中缀调用语法的常规函数,能够为自定义类型定义类似的函数。
如下是Kotlin提供的,用于执行位运算的完整函数列表:

  • shl ——带符号左移,等同Java中<<
  • shr ——带符号右移,等同Java中>>
  • ushr ——无符号右移,等同Java中<<<
  • and ——按位与,等同Java中&
  • or ——按位或,等同Java中|
  • xor ——按位异或,等同Java中^
  • inv ——按位取反,等同Java中~

重载复合赋值运算符

一般状况下,当你在定义想plus这样的运算符函数时,Kotlin不止支持+号运算,也支持+=。像+=,-=等这些运算符被称为复合赋值运算符。看这个例子:

>>> var p1 = Point(10, 20)
>>> p1 += Point(30, 40)
>>> println(p1)
Point(x=40, y=60)
复制代码

这等用于point = point + Point(30, 40) 的写法。固然,这个只对于可变变量有效。
在一些状况下,定义+=运算符能够修改使用它的变量所引用的对象,但不会从新分配引用,将一个元素添加到可变集合,就是一个很好的例子:

>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers)
42
复制代码

若是你定义了一个返回值为Unit,名为plusAssign的函数,Kotlin将会在用到+=运算符的地方调用它,其余二元算术运算符也有命名类似的对应函数:如minusAssigntimeAssign等。
Kotlin标准库为可变集合定义了plusAssign函数,咱们才能像例子中那样使用+=:

operator fun <T> MutableCollection<T> plusAssgin(element: T) {
    this.add(element)
}
复制代码

当你在代码中用到+=的时候,理论上plus和plusAssign均可能被调用。若是在这种状况下,两个函数都有定义且使用,编译器会报错!一种办法是直接使用普通函数的调用方式调用,另外一种办法是用val代替var,这样plusAssign运算就不在适用。可是更建议只定义一种运算函数,plus一般定义返回一个新对象,而plusAssign返回的是以前的对象,根据这个原则选择合适的运算函数定义便可。

Kotlin标准库支持集合的这两种方法。+和-运算符老是返回一个新的集合。+=和-=运算符用于可变集合时,始终就地修改它们:而它们用于只读集合时,或返回一个修改过的副本(这意味着只有当引用只读集合的变量被声明为var的时候,才能使用+=和-=)。做为它们的运算数,可使用单个元素,也可使用元素类型一致的其余集合:

>>> val list = arrayListOf(1, 2)
>>> list += 3
>>> val newList = list + listOf(4, 5) //返回一个新集合
>>> println(list)
[1, 2, 3]
>>> println(newList)
[1, 2, 3, 4, 5]
复制代码

重载一元运算符

重载一元运算符的过程与你在前面看到的方式相同:用预先定义的一个名称来声明(成员函数或扩展函数),并用修饰符operator标记。咱们来看一个例子:

operator fun Point.unaryMinus(): Point = Point(-x, -y)

>>> val p = Point(10, 20)
>>>println(-p)
Point(x=-10, y=-20)
复制代码

用于重载一元运算符的函数,没有任何参数。

可重载的一元算法的运算符

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

当你定义inc和dec函数来重载自增和自减的运算符时,编译器自动支持与普通数字类型的前缀和后缀自增运算符相同的语义。考虑一下用来重载BigDecimal类的++运算符的这个例子:

operator fun BigDecimal.inc() = this + BigDecimal.ONE

>>> var bd = BigDecimal.ZERO
>>> println(bd++)
0
>>> println(++bd)
2
复制代码

后缀运算++首先返回bd变量的当前值,而后执行++,这个和前缀运算相反。打印多的值与使用Int类型的变量所看到的相同,不须要额外作什么特别的事情就能支持。

重载比较运算符

与算术运算符同样,在Kotlin中,能够对任何对象使用比较运算符(==、!=、>、<等),而不只仅限于基本数据类型。不用像Java那样调用equals或compareTo函数,能够直接使用比较运算符。

等号运算符:equals

咱们在教程三中就说到,Kotlin中使用==运算符,它将被转换成equals方法的调用。
使用!=运算符也会被转换成equals函数的调用,明显的差别在于,它们的结果是相反的,和全部其余运算符不一样的是:==和!=能够用于可空运算数,由于这些运算符事实上会检查运算数是否为null。比较 a == b 会检查a是否为非空,若是不是,就调用a.equals(b) 不然,只有两个参数都是空引用,结果才是true。

a == b -> a?.equals(b) ?: (b == null)

对于Point类,由于已经被标记为数据类,equals的实现将会由编译器自动生成。但若是手动实现,name代码能够是这样的:

data class Point(val x: Int, val 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
    }
}
>>> println(Point(10, 20) == Point(10, 20))
true
>>>  println(Point(10, 20) != Point(5, 5))
true
>>>  println(null == Point(10, 20))
false
复制代码

这里使用了恒等运算符(===)来检查参数与调用equals的对象是否相同。恒等运算符与Java中的==运算符彻底相同:检查两个参数是不是同一个对象的引用(若是是基本数据类型,检查他们是不是相同的值)。在实现了equals方法以后,一般会使用这个运算符来优化调用代码。注意,===运算符不能被重载。
equals函数之因此被标记override,那是由于与其余约定不一样的是,这个方法的实现是在Any类中定义的、这也解释了为何你不须要将它标记为operator,Any中的基本方法就已经标记了,并且函数的operator修饰符也适用于全部实现或重写它的方法。还要注意,equals不能实现为扩展方法,由于继承自Any类的实现始终优先于扩展函数。
这个例子显示!=运算符的使用也会转换为equals方法的调用,编译器会自定对返回值取反,所以,你不须要再作别的事情,就能够正常运行。

排序运算符:compareTo

在java中,类能够实现Comparable接口,以便在比较值的算法中使用,例如在查找最大值或排序的时候。接口中定义的compareTo方法用于肯定一个对象是否大于另外一个对象。但在Java中,这个方法的调用没有简明语法,只有基本数据类型能使用<>来比较,全部其余类型都须要明确写为element1.conpareTo(element2)
Kotlin支持相同的Comparable接口。可是可口中定义的compareTo方法能够按约定调用,比较运算符(>,<,<=>=)的使用将被转换为compareTo,compareTo的返回类型必须为Int。p1 < p2 表达式等价于 p1.compareTo(p2) < 0。其余比较运算符的运算方式也是彻底同样的。 咱们假设以Point在y轴上的位置来肯定大小,y越大则Point越大:

data class Point(val x: Int, val y: Int) : Comparable<Point> {
    override fun compareTo(other: Point): Int {
        return y.compareTo(other.y)
    }
}

>>> val p1 = Point(10, 20)
>>> val p2  = Point(30, 40)
>>> val p3  = Point(30, 10)
>>> println(p1 < p2)
true
>>> println(p1 < p3)
false
复制代码

咱们经过实现Comparable接口的方式重载compareTo方法,这样作还能够被Java函数(好比用于对集合进行排序的功能)进行比较,与equals同样,operator修饰符已经被用在了基类的接口中,所以在重写该接口时无需在重复。
全部Java中实现了Comparable接口的类,均可以在Kotlin中使用简洁的运算符语法,不用再增长扩展函数:

>>> println("abc" > "bac")
true
复制代码

集合与区间的终定

经过下标来访问元素:get和set

咱们已经知道在Kotlin中能够用相似Java中数组的方式来访问map中的元素:

val value = map[key]
复制代码

也能够用一样的运算符来改变一个可变map的元素:

mutableMap[key] = newValue
复制代码

来看看它是如何工做的。在Kotlin中,下标运算符是一个约定。使用下标运算符读取元素会被转换为get运算符方法的调用,而且写入元素将调用set。Map和MutableMap的接口已经定义了这些方法。让咱们看看如何给自定义的类添加相似的方法。
可使用方括号来引用点的坐标,p[0]访问x坐标, p[1]访问y坐标:

operator fun Point.get(index: Int): Int {
    return when (index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}
>>> val p = Point(10, 20)
>>> println(p[1])
20
复制代码

你只须要定义一个名为get的函数,并标记operator以后,像p[1]这样的表达式,其中p具备类型Point,将被转换为get方法的调用。
x[a, b] -> x.get(a ,b)

get的参数能够是任何类型,而不仅是Int。例如,当你对map使用下标运算符时,参数类型是键的类型,它能够是任意类型。还能够定义具备多个参数的get方法。例如,若是要实现一个类来表示二维数组或矩阵,你能够定义一个方法,例如operator fun get(rowIndex: Int, colIndex: Int) ,而后用matrix[row, col] 来调用。若是须要使用不一样的键类型访问集合,也可使用不一样的参数类型定义多个重载的get方法。
咱们也能够用相似的方法定义一个函数,这样就可使用方括号语法更改给定下标处的值。Point类是不可变的,因此定义Point的这种方法是没有意义的。做为例子,咱们来定义另外一个类来表示一个可变的点:

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)
复制代码

这个例子也很简单,只需定义一个名为set的函数,就能够在赋值语句中使用下标运算符。set的最后一个参数用来接收赋值语句中等号右边的值,其余参数做为方括号内的下标。
x[a ,b] = c -> x.set(a, b, c)

in 的约定

集合支持的另外一个运算符是in运算符,用于检查某个对象是否属于集合。相应的函数叫作contains。咱们来实现如下,使用in运算符来检查点是否属于一个矩形:

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(5, 5) in rect)
false
复制代码

in右边的对象将会调用contains函数,in左边的对象将会做为函数入参。
a in c -> c.contains(a)

在Rectangle.contains的实现中,咱们用到了的标准库的until函数,来构建一个开区间,而后使用运算符in来检查某个点是否属于这个区间。
开区间是不包含最后一个点的区间。例如,若是用10..20构建一个普通的区间(闭区间),该区间则包括10到20的全部数字,包括20。开区间10 until 20 包括从10到19的数字,但不包括20。矩形类一般定义成这样,它的底部和右侧坐标不是矩形的一部分,所以在这里使用开区间是合适的。

rangeTo的约定

要建立一个区间,请使用..语法。..运算符是调用rangeTo函数的一个简洁方法。
start..end -> start.rangeTo(end)

rangeTo函数返回一个区间。你能够为本身的类定义这个运算符。可是,若是该类实现了Comparable接口,那么就不须要了:你能够经过Kotlin标准库建立一个任意可比较元素的区间,这个库定义了能够用于任何可比较元素的rangeTo函数:

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
复制代码

这个函数返回一个区间,能够用来检测其余一些元素是否属于它。

rangeTo运算符的优先级低于算术运算符,可是最好把参数括起来以避免混淆:

>>> val n = 9
>>> println(0..(n + 1))
0..10
复制代码

还要注意,表达式0..n.forEach{}不会被编译,必须把区间表达式括起来才能调用它的方法:

>>> (0..n).forEach { print(it) }
0123456789
复制代码

在for循环中使用iterator的约定

在Kotlin中,for循环中也可使用in运算符,和作区间检查同样。可是在这种状况下它的含义是不一样的:它被用来执行迭代。这意味着一个诸如for(x in list) {}将被转换成list.iterator() 的调用,而后就像在Java中同样,在它上面重复调用hasNext和next方法。
在Kotlin中,这也是一种约定,这意味着iterator方法能够被定义为扩展函数。这就解释了为何能够遍历一个常规的Java字符串:标准库已经为CharSequence定义了一个扩展函数iterator,而它是String的父类:

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
}

>>> for (c in "abc") {}
复制代码

解构声明和组件函数

解构声明容许你展开单个复合值,并使用它来初始化多个单独的变量。来看看它是怎样工做的:

>>> val p = Point(10, 20)
>>> val (x, y) = p
>>> println(x)
10
>>> println(y)
20
复制代码

一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。
事实上,解构声明再次用到了约定的原理。要在结构声明中初始化每一个变量,将调用名为componentN的函数,其中N是声明中变量的位置。换句话说,前面的例子能够被转换成:
val (a, b) = p -> val a = p.component1(); val b = p.component2()
对于数据类,编译器为每一个在主构造方法中声明的属性生成一个componentN函数。下面的例子显示了如何手动为非数据类声明这些功能:

class Point(val x: Int,val y: Int) {
    operator fun component1() = x
    operator fun component2() = 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)
}

>>> val (name, ext) = splitFilename("example.kt")
>>> println(name)
example
>>> println(ext)
kt
复制代码

固然,不可能定义无线数量的componentN函数,这样这个语法就能够与任意数量的集合一块儿工做了,但这也没用。标准库只容许使用此语法来访问一个对象的前个元素。
让一个函数能返回多个值有更简单的方法,是使用标准库中的Pair和Triple类,在语义表达上这种方式会差一点,由于这些类也不知道它会返回的对象中包含什么,但由于不须要定义本身的类因此能够少写代码。

解构声明和循环

解构声明不只能够做用函数中的顶层语句,还能够用在其余能够声明变量的地方,例如in循环。一个很好的例子,是枚举map中的条目,下面是一个小例子:

fun printEntries(mapL Map<String, String>) {
    for ((key, value) in map){
        println("$key -> $value")
    }
}

>>> val map = mapOf("Oracle" to "Java", "JetBrans" to "Kotlin")
>>> printEntries(map)
Oracle -> Java
JetBrans -> Kotlin
复制代码

这个简单的例子用到了两个Kotlin的约定:一个是迭代一个对象,另外一个是用于解构声明。Kotlin标准库给map增长了一个扩展的iterator函数,用来返回Entry条目的迭代器。所以,与Java不一样的是,能够直接迭代map。它还包含Map.Entry上的扩展函数component1和component2,分别返回它的键和值。实际上,前面的循环被转换成了这样的代码:

for (entry in map.entries){
    val key = entry.component1()
    val value = entry.component2()
    //...
}
复制代码

重用属性访问的逻辑:委托属性

委托属性的基本操做

委托属性的基本语法时这样的:

class Foo {
    var p: Type by Delegate()
}
复制代码

属性p将它的访问器逻辑委托给了另外一个对象:这里是Delegate类的一个新实例。经过关键字by对其后的表达式求值来获取这个对象,关键字by能够用于任何符合属性委托约定规则的对象。
编译器建立一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始属性p会委托给该实例。为了简单起见,咱们把它称为delegate:

class Foo {
    private val delegate = Delegate() //编译器自动生成
    var p: Type //p的访问交给delegate
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(...)
}

复制代码

按照约定,Delegate类必须具备getValue和setValue方法(后者仅适用于可变属性)。它们能够是成员函数,也能够是扩展函数。为了让例子看起来更简洁,这里咱们省略掉参数。准确的函数签名将在以后接招。Delegate类的简单实现差很少应该是这样的:

class Delegate{
    operator fun getValue(...) {...}  //实现getter逻辑
    operator fun setValue(..., value: Type) {...} //实现setter逻辑
}

class Foo{
    var p: Type by Delegate() //属性关联委托对象
}

>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue
复制代码

能够把foo.p做为普通的属性使用,事实上,它将调用Delegate类型的辅助属性的方法。为了研究这种机制如何在实践中使用,咱们首先看一个委托属性展现威力的例子:库对惰性初始化的支持。

使用委托属性:惰性初始化和 by lazy()

惰性初始化是一种常见的模式,知道在第一次访问该属性的时候,才根据须要建立对象的一部分。当初始化过程消耗大量资源而且在使用对象时并不老是须要数据时,这个很是有用。
举个例子,一个Person类,能够用来访问一我的写的邮件列表。邮件存储在数据库中,访问比较耗时。你但愿只有在首次访问时才加载邮件,并只执行一次。假设你已经有函数loadEmails,用来从数据库中检索电子邮件:

class Email {/*...*/}
fun loadEmail(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}
复制代码

下面展现如何使用额外的_emails属性来实现惰性加载,在没有加载以前为null,而后加载为邮件列表:

class Person(val name: String) {
    private var _emails: List<Email>? = null
    val emails: List<Email>
        get() {
            if(_emails == null) {
                _emails = loadEmails(this)
            }
            return _emials!!
        }
}
>>> val p = Person("Alice")
>>> p.emails   //第一次加载会访问邮件
Load emails for Alice
>>> p.emails
复制代码

这里使用了所谓的属性支持。你有一个属性_emails来存储这个值,而另外一个emails,用来提供对属性的读取访问。你须要使用两个属性,由于属性具备不一样类型:_emails可空,而emails为非空。这种技术常常会使用到,值得熟练掌握。
但这个代码有点啰嗦:要是有几个惰性属性那得有多长。并且,它并不老是正常运行:这个实现不是线程安全的。Kotlin提供了更好的解决方案。
使用委托属性会让代码变得简单得多,能够封装用于存储值得支持属性和确保该值只被初始化一次的逻辑。在这里可使用标准库函数lazy放回的委托。

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}
复制代码

lazy函数返回一个对象,该对象具备一个名为getValue且签名正确的方法,所以能够把它与by关键字一块儿使用来建立一个委托属性。lazy的参数是一个lambda,能够调用它来初始化这个值。默认状况下,lazy函数是线程安全的,若是须要,能够设置其余选项来告诉它要使用哪一个锁,或者彻底避开同步,若是该类永远不会再多线程中使用。

实现委托属性

要了解委托属性的实现方式,让咱们来看另外一个例子:当一个对象的属性更改时通知监听器。这在许多不一样的状况下都颇有用:例如,当对象显示在UI时,你但愿在对象变化时UI能自动刷新。Java具备用于此类通知的标准机制:PropertyChangeSupport和PropertyChangeEvent类。让咱们看看在Kotlin中不使用委托属性的状况下,该如何使用它们,而后咱们再将代码重构为用委托属性的方式。
PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,你一般须要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。
为了不要在每一个类中添加这个字段,你须要建立一个小的工具类,用来存储PropertyChangeSupport的实例并监听属性更改。以后,你的类会继承这个工具类,以访问changeSupport。

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

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

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}
复制代码

如今咱们来写一个Person类,定义一个只读属性(做为一我的的名字,通常不会随时更改)和两个可写属性:年龄和工资。当这我的的年龄或工资发生变化时,这个类将通知它的监听器。

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
    var age: Int = age
        set(newValue) {
            val oldValue = field  //field标识符访问支持字段
            field = newValue
            changeSupport.firePropertyChange("age", oldValue, newValue)  //属性变化时通知监听器
        }

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

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    //添加监听器
    p.addPropertyChangeListener(PropertyChangeListener { event ->
        println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
    })
    p.age = 35
    p.salary = 2100
}

//输出
Property age changed from 34 to 35
Property salary changed from 2000 to 2100
复制代码

setter中有不少重复的代码,咱们来尝试提取一个类,用来存储这个属性的值并发起通知。

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", age, changeSupport)

    var salary: Int
        get() = _salary.getValue()
        set(value) = _salary.setValue(value)
}
复制代码

如今,你应该已经差很少理解了在Kotlin中,委托属性是如何工做的。你建立了一个保存属性值的类,并在修改属性时自动触发更改通知。你删除了重复的逻辑代码,可是须要至关多的样板代码来为每一个属性建立ObservableProperty实例,并把getter和setter委托给它。Kotlin的委托属性功能可让你摆脱这些样板代码。可是在此以前,你须要更改ObservableProperty方法的签名,来匹配Kotlin约定所需的方法。

class ObservableProperty(
        var propValue: 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)
    }
}
复制代码

与以前的版本相比,此次代码作了一些更改:

  • 如今,按照也回到那个的须要,getValue和setValue函数被标记了operator
  • 这些函数加了两个参数:一个用于接收属性的实例,用来设置或读取属性,另外一个用于表示属性自己。这个属性类型为KProperty(以后章节会详细介绍它),如今你只须要知道能够经过KProperty.name的方式来访问该属性的名称。
  • 把name属性从主构造方法中删除了,由于如今已经能够经过KProperty访问属性名称。

终于,你能够见识Kotlin委托属性的神奇了,来看看代码变短了多少?

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类进行比较:使用委托属性时生成的代码很是相似,右边的对象被称为委托。Kotlin会自动将委托存储在隐藏的属性中,并在访问或修改属性时调用委托的getValue和setValue。
你不用手动去实现可观察的属性逻辑,可使用Kotlin标准库,它已经包含了相似ObserverProperty的类。标准库和这里使用的PropertyChangeSupport类没有耦合,所以,你须要传递一个lambda,来告诉它如何通知属性值得更改,能够这样作:

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右边的表达式不必定是新建立的实例,也能够是函数调用,另外一个属性或任何其余表达式,只要这个表达式的值,是可以被编译器用正确的参数类型来调用getValue和setValue的对象。与其余约定同样,getValue和setValue能够是对象本身生命的方法或扩展函数。
注意,为了让示例保持简单,咱们只展现了如何使用类型为Int的委托属性,委托属性机制实际上是通用的,适用于任何其余类型。

委托属性的变换规则

让咱们来总结一下委托属性是怎样工做的,假设你已经有了一个具备委托属性的类:

class C {
    var p: Type by MyDelegate()
}

val c = C()
复制代码

MyDelegate实例会保存到一个隐藏的属性中,它被称为<delegate>。编译器也将用一个KProperty类型的对象来表明这个属性,它被称为<property>
编译器生成的代码以下:

class C {
    private val <delegate> = MyDelegate()
    
    var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <delegate>.setValue(this, <property>, value)
}
复制代码

所以,在每一个属性访问器中,编译器都会生成对应的getValue和setValue方法: val x = c.prop -> val x = <delegate>.getValue(c, <property>)
c,prop = x -> <delegate>.setValue(c, <property>, x)

这个机制很是简单,但它能够实现许多有趣的场景。你能够自定义存储该属性值得位置(map、数据库表或者用户会话的Cookie中),以及在访问该属性时作点什么(好比添加验证、更改通知等)。

在map中保存属性值

委托属性发挥做用的另外一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时候被称为自定(expando)对象。例如,考虑一个联系人管理系统,能够用来存储有关联系人的任意信息。系统中的每一个人都有一些属性须要特殊处理(例如名字),以及每一个人特有的数量任意的额外属性(例如,最小的孩子的生日)。 实现这种系统的一种方法是将人的全部属性存储在map中,不肯定提供属性,来访问须要特殊处理的信息。来看个例子:

class Person {
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    
    val name: String
        get() = _attributes["name"]!!
}

fun main(args: Array<String>) {
    val p = Person()
    val data = mapOf("name" to "Dimtry", "company" to "JetBrans")
    for ((attrName, value) in data) {
        p.setAttribute(attrName, value)
    }
    println(p.name)
}

//输出
Dimtry
复制代码

这里使用了一个通用的API来吧数据加载到对象中(在实际项目中,能够是JSON反序列化或相似的方法),而后使用特定的API来访问一个属性的值。把它改成委托属性很是简单,能够直接将map放在by关键字后面。

class Person {
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}
复制代码

由于标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数,因此这里能够直接这样用。属性的名称将自动用做map中的键,属性值做为map中的值。改动前p.name隐藏了_attributes.getValue(p, prop)的调用,改动后变为_attributes[prop.name]

相关文章
相关标签/搜索