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]

相關文章
相關標籤/搜索