寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kotlin的同學(xué)。系列文章的知識點會以《Kotlin實戰(zhàn)》這本書中順序編寫,在將書中知識點展示出來同時,我也會添加對應(yīng)的Java代碼用于對比學(xué)習(xí)和更好的理解。
Kotlin教程(一)基礎(chǔ)
Kotlin教程(二)函數(shù)
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其他約定
Kotlin教程(八)高階函數(shù)
Kotlin教程(九)泛型
如你所知,Java在標(biāo)準(zhǔn)庫中有一些與特定的類相關(guān)聯(lián)的語言特性。例如,實現(xiàn)了java.lang.Iterable接口的獨享可以在for循環(huán)中使用,實現(xiàn)了java.lang.AutoCloseable接口的對象可以在try-with-resources語句中使用。
Kotlin也有許多特性的原理非常類似,通過調(diào)用自己代碼中定義的函數(shù),來實現(xiàn)特定語言結(jié)構(gòu)。但是,在Kotlin中,這些功能與特定的函數(shù)命名相關(guān),而不是與特定的類型綁定。
這一章我們會用到一個UI框架中常見的類Point來演示,來看下定義:
data class Ponit(val x: Int, val y: Int)
重載算術(shù)運算符
在Java中,全套的算數(shù)運算只能用于基本數(shù)據(jù)類型,+運算符可以與String值一起使用。但是,這些運算符在其他一些情況下用起來也很方便。例如,在使用哪個BigInteger類處理數(shù)字的時候,使用+號就比掉用add方法顯得更為優(yōu)雅:給集合添加元素的時候,你可能也在想要是能用+=運算符就好了,在Kotlin中,你就可以這樣做。
重載二元算術(shù)運算
我們來支持第一個運算,把兩個點加到一起:
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)
用于重載運算符的所有函數(shù)都需要使用operator關(guān)鍵字標(biāo)記,表示你把這個函數(shù)作為相應(yīng)的約定的實現(xiàn),并且不是碰巧地定義了同名函數(shù)。
使用operator修飾符聲明plus函數(shù)之后,你就可以直接使用+號來求和了。實際上調(diào)用的時plus函數(shù)a + b -> a.plus(b)。
除了聲明成為一個成員函數(shù)外,也可以定義為一個擴展函數(shù),同樣有效:
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
Kotlin中可重載的二元算術(shù)運算符
| 表達(dá)式 | 函數(shù)名 |
|---|---|
| a * b | times |
| a / b | div |
| a % b | mod |
| a + b | plus |
| a - b | minus |
自定義類型的運算符,基本上和與標(biāo)準(zhǔn)數(shù)字類型的運算符有著相同的優(yōu)先級。例如a + b * c,乘法將之中在加號之前執(zhí)行。運算符*、 /和%具有相同的優(yōu)先級,高于+和-運算符的優(yōu)先級。
運算符函數(shù)和Java
從Java調(diào)用Kotlin運算符非常容易:因為每個重載的運算符都被定義為一個函數(shù),可以像普通函數(shù)那樣調(diào)用它們。當(dāng)從Kotlin調(diào)用Java的時候,只要Java代碼中存在函數(shù)名和參數(shù)數(shù)量都匹配的函數(shù),就可以在Kotlin中使用。如果Java已經(jīng)存在類似的方法,但是方法名不同,可以通過擴展函數(shù)來修正這個函數(shù)名,用來代替現(xiàn)有的Java方法。
當(dāng)你定義一個運算符的時候,不要求兩個運算數(shù)是相同的類型,例如,讓我們定義一個運算符,它允許你用一個數(shù)字來縮放一個點,可以用它在不同坐標(biāo)系之間做轉(zhuǎn)換:
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。
運算符函數(shù)的返回類型可以不同于任一運算數(shù)類型,例如,可以定義一個運算符,通過多次重復(fù)單個字符來創(chuàng)建字符串:
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
>>> println('a' * 3)
aaa
這個運算符接收一個Char作為左值,Int作為右值,然后返回一個String類型。
和普通的函數(shù)一樣,可以重載operator函數(shù):可以定義多個同名的,但參數(shù)類型不同的方法。
沒有用于位運算的特殊運算符
Kotlin沒有為標(biāo)準(zhǔn)數(shù)字類型定義任何位運算符。因此,也不允許你為自定義類型定義它們,相反,它使用支持中綴調(diào)用語法的常規(guī)函數(shù),可以為自定義類型定義相似的函數(shù)。
以下是Kotlin提供的,用于執(zhí)行位運算的完整函數(shù)列表:
- shl ——帶符號左移,等同Java中<<
- shr ——帶符號右移,等同Java中>>
- ushr ——無符號右移,等同Java中<<<
- and ——按位與,等同Java中&
- or ——按位或,等同Java中|
- xor ——按位異或,等同Java中^
- inv ——按位取反,等同Java中~
重載復(fù)合賦值運算符
通常情況下,當(dāng)你在定義想plus這樣的運算符函數(shù)時,Kotlin不止支持+號運算,也支持+=。像+=,-=等這些運算符被稱為復(fù)合賦值運算符??催@個例子:
>>> var p1 = Point(10, 20)
>>> p1 += Point(30, 40)
>>> println(p1)
Point(x=40, y=60)
這等用于point = point + Point(30, 40) 的寫法。當(dāng)然,這個只對于可變變量有效。
在一些情況下,定義+=運算符可以修改使用它的變量所引用的對象,但不會重新分配引用,將一個元素添加到可變集合,就是一個很好的例子:
>>> val numbers = ArrayList<Int>()
>>> numbers += 42
>>> println(numbers)
42
如果你定義了一個返回值為Unit,名為plusAssign的函數(shù),Kotlin將會在用到+=運算符的地方調(diào)用它,其他二元算術(shù)運算符也有命名相似的對應(yīng)函數(shù):如minusAssign、timeAssign等。
Kotlin標(biāo)準(zhǔn)庫為可變集合定義了plusAssign函數(shù),我們才能像例子中那樣使用+=:
operator fun <T> MutableCollection<T> plusAssgin(element: T) {
this.add(element)
}
當(dāng)你在代碼中用到+=的時候,理論上plus和plusAssign都可能被調(diào)用。如果在這種情況下,兩個函數(shù)都有定義且使用,編譯器會報錯!一種辦法是直接使用普通函數(shù)的調(diào)用方式調(diào)用,另一種辦法是用val代替var,這樣plusAssign運算就不在適用。但是更建議只定義一種運算函數(shù),plus通常定義返回一個新對象,而plusAssign返回的是之前的對象,根據(jù)這個原則選擇合適的運算函數(shù)定義即可。
Kotlin標(biāo)準(zhǔn)庫支持集合的這兩種方法。+和-運算符總是返回一個新的集合。+=和-=運算符用于可變集合時,始終就地修改它們:而它們用于只讀集合時,或返回一個修改過的副本(這意味著只有當(dāng)引用只讀集合的變量被聲明為var的時候,才能使用+=和-=)。作為它們的運算數(shù),可以使用單個元素,也可以使用元素類型一致的其他集合:
>>> 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]
重載一元運算符
重載一元運算符的過程與你在前面看到的方式相同:用預(yù)先定義的一個名稱來聲明(成員函數(shù)或擴展函數(shù)),并用修飾符operator標(biāo)記。我們來看一個例子:
operator fun Point.unaryMinus(): Point = Point(-x, -y)
>>> val p = Point(10, 20)
>>>println(-p)
Point(x=-10, y=-20)
用于重載一元運算符的函數(shù),沒有任何參數(shù)。
可重載的一元算法的運算符
| 表達(dá)式 | 函數(shù)名 |
|---|---|
| +a | unaryPlus |
| -a | unaryMinus |
| !a | not |
| ++a, a++ | inc |
| --a, a-- | dec |
當(dāng)你定義inc和dec函數(shù)來重載自增和自減的運算符時,編譯器自動支持與普通數(shù)字類型的前綴和后綴自增運算符相同的語義??紤]一下用來重載BigDecimal類的++運算符的這個例子:
operator fun BigDecimal.inc() = this + BigDecimal.ONE
>>> var bd = BigDecimal.ZERO
>>> println(bd++)
0
>>> println(++bd)
2
后綴運算++首先返回bd變量的當(dāng)前值,然后執(zhí)行++,這個和前綴運算相反。打印多的值與使用Int類型的變量所看到的相同,不需要額外做什么特別的事情就能支持。
重載比較運算符
與算術(shù)運算符一樣,在Kotlin中,可以對任何對象使用比較運算符(==、!=、>、<等),而不僅僅限于基本數(shù)據(jù)類型。不用像Java那樣調(diào)用equals或compareTo函數(shù),可以直接使用比較運算符。
等號運算符:equals
我們在教程三中就說到,Kotlin中使用==運算符,它將被轉(zhuǎn)換成equals方法的調(diào)用。
使用!=運算符也會被轉(zhuǎn)換成equals函數(shù)的調(diào)用,明顯的差異在于,它們的結(jié)果是相反的,和所有其他運算符不同的是:==和!=可以用于可空運算數(shù),因為這些運算符事實上會檢查運算數(shù)是否為null。比較 a == b 會檢查a是否為非空,如果不是,就調(diào)用a.equals(b) 否則,只有兩個參數(shù)都是空引用,結(jié)果才是true。
a == b -> a?.equals(b) ?: (b == null)
對于Point類,因為已經(jīng)被標(biāo)記為數(shù)據(jù)類,equals的實現(xiàn)將會由編譯器自動生成。但如果手動實現(xiàn),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
這里使用了恒等運算符(===)來檢查參數(shù)與調(diào)用equals的對象是否相同。恒等運算符與Java中的==運算符完全相同:檢查兩個參數(shù)是否是同一個對象的引用(如果是基本數(shù)據(jù)類型,檢查他們是否是相同的值)。在實現(xiàn)了equals方法之后,通常會使用這個運算符來優(yōu)化調(diào)用代碼。注意,===運算符不能被重載。
equals函數(shù)之所以被標(biāo)記override,那是因為與其他約定不同的是,這個方法的實現(xiàn)是在Any類中定義的、這也解釋了為什么你不需要將它標(biāo)記為operator,Any中的基本方法就已經(jīng)標(biāo)記了,而且函數(shù)的operator修飾符也適用于所有實現(xiàn)或重寫它的方法。還要注意,equals不能實現(xiàn)為擴展方法,因為繼承自Any類的實現(xiàn)始終優(yōu)先于擴展函數(shù)。
這個例子顯示!=運算符的使用也會轉(zhuǎn)換為equals方法的調(diào)用,編譯器會自定對返回值取反,因此,你不需要再做別的事情,就可以正常運行。
排序運算符:compareTo
在java中,類可以實現(xiàn)Comparable接口,以便在比較值的算法中使用,例如在查找最大值或排序的時候。接口中定義的compareTo方法用于確定一個對象是否大于另一個對象。但在Java中,這個方法的調(diào)用沒有簡明語法,只有基本數(shù)據(jù)類型能使用<和>來比較,所有其他類型都需要明確寫為element1.conpareTo(element2)。
Kotlin支持相同的Comparable接口。但是可口中定義的compareTo方法可以按約定調(diào)用,比較運算符(>,<,<=和>=)的使用將被轉(zhuǎn)換為compareTo,compareTo的返回類型必須為Int。p1 < p2 表達(dá)式等價于 p1.compareTo(p2) < 0。其他比較運算符的運算方式也是完全一樣的。
我們假設(shè)以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
我們通過實現(xiàn)Comparable接口的方式重載compareTo方法,這樣做還可以被Java函數(shù)(比如用于對集合進行排序的功能)進行比較,與equals一樣,operator修飾符已經(jīng)被用在了基類的接口中,因此在重寫該接口時無需在重復(fù)。
所有Java中實現(xiàn)了Comparable接口的類,都可以在Kotlin中使用簡潔的運算符語法,不用再增加擴展函數(shù):
>>> println("abc" > "bac")
true
集合與區(qū)間的終定
通過下標(biāo)來訪問元素:get和set
我們已經(jīng)知道在Kotlin中可以用類似Java中數(shù)組的方式來訪問map中的元素:
val value = map[key]
也可以用同樣的運算符來改變一個可變map的元素:
mutableMap[key] = newValue
來看看它是如何工作的。在Kotlin中,下標(biāo)運算符是一個約定。使用下標(biāo)運算符讀取元素會被轉(zhuǎn)換為get運算符方法的調(diào)用,并且寫入元素將調(diào)用set。Map和MutableMap的接口已經(jīng)定義了這些方法。讓我們看看如何給自定義的類添加類似的方法。
可以使用方括號來引用點的坐標(biāo),p[0]訪問x坐標(biāo), p[1]訪問y坐標(biāo):
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的函數(shù),并標(biāo)記operator之后,像p[1]這樣的表達(dá)式,其中p具有類型Point,將被轉(zhuǎn)換為get方法的調(diào)用。
x[a, b] -> x.get(a ,b)
get的參數(shù)可以是任何類型,而不只是Int。例如,當(dāng)你對map使用下標(biāo)運算符時,參數(shù)類型是鍵的類型,它可以是任意類型。還可以定義具有多個參數(shù)的get方法。例如,如果要實現(xiàn)一個類來表示二維數(shù)組或矩陣,你可以定義一個方法,例如operator fun get(rowIndex: Int, colIndex: Int) ,然后用matrix[row, col] 來調(diào)用。如果需要使用不同的鍵類型訪問集合,也可以使用不同的參數(shù)類型定義多個重載的get方法。
我們也可以用類似的方法定義一個函數(shù),這樣就可以使用方括號語法更改給定下標(biāo)處的值。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的函數(shù),就可以在賦值語句中使用下標(biāo)運算符。set的最后一個參數(shù)用來接收賦值語句中等號右邊的值,其他參數(shù)作為方括號內(nèi)的下標(biāo)。
x[a ,b] = c -> x.set(a, b, c)
in 的約定
集合支持的另一個運算符是in運算符,用于檢查某個對象是否屬于集合。相應(yīng)的函數(shù)叫做contains。我們來實現(xiàn)以下,使用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右邊的對象將會調(diào)用contains函數(shù),in左邊的對象將會作為函數(shù)入?yún)ⅰ?br>
a in c -> c.contains(a)
在Rectangle.contains的實現(xiàn)中,我們用到了的標(biāo)準(zhǔn)庫的until函數(shù),來構(gòu)建一個開區(qū)間,然后使用運算符in來檢查某個點是否屬于這個區(qū)間。
開區(qū)間是不包含最后一個點的區(qū)間。例如,如果用10..20構(gòu)建一個普通的區(qū)間(閉區(qū)間),該區(qū)間則包括10到20的所有數(shù)字,包括20。開區(qū)間10 until 20 包括從10到19的數(shù)字,但不包括20。矩形類通常定義成這樣,它的底部和右側(cè)坐標(biāo)不是矩形的一部分,因此在這里使用開區(qū)間是合適的。
rangeTo的約定
要創(chuàng)建一個區(qū)間,請使用..語法。..運算符是調(diào)用rangeTo函數(shù)的一個簡潔方法。
start..end -> start.rangeTo(end)
rangeTo函數(shù)返回一個區(qū)間。你可以為自己的類定義這個運算符。但是,如果該類實現(xiàn)了Comparable接口,那么就不需要了:你可以通過Kotlin標(biāo)準(zhǔn)庫創(chuàng)建一個任意可比較元素的區(qū)間,這個庫定義了可以用于任何可比較元素的rangeTo函數(shù):
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
這個函數(shù)返回一個區(qū)間,可以用來檢測其他一些元素是否屬于它。
rangeTo運算符的優(yōu)先級低于算術(shù)運算符,但是最好把參數(shù)括起來以免混淆:
>>> val n = 9
>>> println(0..(n + 1))
0..10
還要注意,表達(dá)式0..n.forEach{}不會被編譯,必須把區(qū)間表達(dá)式括起來才能調(diào)用它的方法:
>>> (0..n).forEach { print(it) }
0123456789
在for循環(huán)中使用iterator的約定
在Kotlin中,for循環(huán)中也可以使用in運算符,和做區(qū)間檢查一樣。但是在這種情況下它的含義是不同的:它被用來執(zhí)行迭代。這意味著一個諸如for(x in list) {}將被轉(zhuǎn)換成list.iterator() 的調(diào)用,然后就像在Java中一樣,在它上面重復(fù)調(diào)用hasNext和next方法。
在Kotlin中,這也是一種約定,這意味著iterator方法可以被定義為擴展函數(shù)。這就解釋了為什么可以遍歷一個常規(guī)的Java字符串:標(biāo)準(zhǔn)庫已經(jīng)為CharSequence定義了一個擴展函數(shù)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") {}
解構(gòu)聲明和組件函數(shù)
解構(gòu)聲明允許你展開單個復(fù)合值,并使用它來初始化多個單獨的變量。來看看它是怎樣工作的:
>>> val p = Point(10, 20)
>>> val (x, y) = p
>>> println(x)
10
>>> println(y)
20
一個解構(gòu)聲明看起來像一個普通的變量聲明,但它在括號中有多個變量。
事實上,解構(gòu)聲明再次用到了約定的原理。要在結(jié)構(gòu)聲明中初始化每個變量,將調(diào)用名為componentN的函數(shù),其中N是聲明中變量的位置。換句話說,前面的例子可以被轉(zhuǎn)換成:
val (a, b) = p -> val a = p.component1(); val b = p.component2()
對于數(shù)據(jù)類,編譯器為每個在主構(gòu)造方法中聲明的屬性生成一個componentN函數(shù)。下面的例子顯示了如何手動為非數(shù)據(jù)類聲明這些功能:
class Point(val x: Int,val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
解構(gòu)聲明主要使用場景之一,是從一個函數(shù)返回多個值,這個非常有用。如果要這樣做,可以定義一個數(shù)據(jù)類來保存返回所需的值,并將它作為函數(shù)的返回類型。在調(diào)用函數(shù)后,可以用解構(gòu)聲明的方式,來輕松地展開它,使用其中的值。舉個例子,讓我們編寫一個簡單的函數(shù),來將一個文件名分割成名字和擴展名:
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
當(dāng)然,不可能定義無線數(shù)量的componentN函數(shù),這樣這個語法就可以與任意數(shù)量的集合一起工作了,但這也沒用。標(biāo)準(zhǔn)庫只允許使用此語法來訪問一個對象的前五個元素。
讓一個函數(shù)能返回多個值有更簡單的方法,是使用標(biāo)準(zhǔn)庫中的Pair和Triple類,在語義表達(dá)上這種方式會差一點,因為這些類也不知道它會返回的對象中包含什么,但因為不需要定義自己的類所以可以少寫代碼。
解構(gòu)聲明和循環(huán)
解構(gòu)聲明不僅可以作用函數(shù)中的頂層語句,還可以用在其他可以聲明變量的地方,例如in循環(huán)。一個很好的例子,是枚舉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的約定:一個是迭代一個對象,另一個是用于解構(gòu)聲明。Kotlin標(biāo)準(zhǔn)庫給map增加了一個擴展的iterator函數(shù),用來返回Entry條目的迭代器。因此,與Java不同的是,可以直接迭代map。它還包含Map.Entry上的擴展函數(shù)component1和component2,分別返回它的鍵和值。實際上,前面的循環(huán)被轉(zhuǎn)換成了這樣的代碼:
for (entry in map.entries){
val key = entry.component1()
val value = entry.component2()
//...
}
重用屬性訪問的邏輯:委托屬性
委托屬性的基本操作
委托屬性的基本語法時這樣的:
class Foo {
var p: Type by Delegate()
}
屬性p將它的訪問器邏輯委托給了另一個對象:這里是Delegate類的一個新實例。通過關(guān)鍵字by對其后的表達(dá)式求值來獲取這個對象,關(guān)鍵字by可以用于任何符合屬性委托約定規(guī)則的對象。
編譯器創(chuàng)建一個隱藏的輔助屬性,并使用委托對象的實例進行初始化,初始屬性p會委托給該實例。為了簡單起見,我們把它稱為delegate:
class Foo {
private val delegate = Delegate() //編譯器自動生成
var p: Type //p的訪問交給delegate
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
按照約定,Delegate類必須具有g(shù)etValue和setValue方法(后者僅適用于可變屬性)。它們可以是成員函數(shù),也可以是擴展函數(shù)。為了讓例子看起來更簡潔,這里我們省略掉參數(shù)。準(zhǔn)確的函數(shù)簽名將在之后接招。Delegate類的簡單實現(xiàn)差不多應(yīng)該是這樣的:
class Delegate{
operator fun getValue(...) {...} //實現(xiàn)getter邏輯
operator fun setValue(..., value: Type) {...} //實現(xiàn)setter邏輯
}
class Foo{
var p: Type by Delegate() //屬性關(guān)聯(lián)委托對象
}
>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue
可以把foo.p作為普通的屬性使用,事實上,它將調(diào)用Delegate類型的輔助屬性的方法。為了研究這種機制如何在實踐中使用,我們首先看一個委托屬性展示威力的例子:庫對惰性初始化的支持。
使用委托屬性:惰性初始化和 by lazy()
惰性初始化是一種常見的模式,知道在第一次訪問該屬性的時候,才根據(jù)需要創(chuàng)建對象的一部分。當(dāng)初始化過程消耗大量資源并且在使用對象時并不總是需要數(shù)據(jù)時,這個非常有用。
舉個例子,一個Person類,可以用來訪問一個人寫的郵件列表。郵件存儲在數(shù)據(jù)庫中,訪問比較耗時。你希望只有在首次訪問時才加載郵件,并只執(zhí)行一次。假設(shè)你已經(jīng)有函數(shù)loadEmails,用來從數(shù)據(jù)庫中檢索電子郵件:
class Email {/*...*/}
fun loadEmail(person: Person): List<Email> {
println("Load emails for ${person.name}")
return listOf(/*...*/)
}
下面展示如何使用額外的_emails屬性來實現(xiàn)惰性加載,在沒有加載之前為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為非空。這種技術(shù)經(jīng)常會使用到,值得熟練掌握。
但這個代碼有點啰嗦:要是有幾個惰性屬性那得有多長。而且,它并不總是正常運行:這個實現(xiàn)不是線程安全的。Kotlin提供了更好的解決方案。
使用委托屬性會讓代碼變得簡單得多,可以封裝用于存儲值得支持屬性和確保該值只被初始化一次的邏輯。在這里可以使用標(biāo)準(zhǔn)庫函數(shù)lazy放回的委托。
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy函數(shù)返回一個對象,該對象具有一個名為getValue且簽名正確的方法,因此可以把它與by關(guān)鍵字一起使用來創(chuàng)建一個委托屬性。lazy的參數(shù)是一個lambda,可以調(diào)用它來初始化這個值。默認(rèn)情況下,lazy函數(shù)是線程安全的,如果需要,可以設(shè)置其他選項來告訴它要使用哪個鎖,或者完全避開同步,如果該類永遠(yuǎn)不會再多線程中使用。
實現(xiàn)委托屬性
要了解委托屬性的實現(xiàn)方式,讓我們來看另一個例子:當(dāng)一個對象的屬性更改時通知監(jiān)聽器。這在許多不同的情況下都很有用:例如,當(dāng)對象顯示在UI時,你希望在對象變化時UI能自動刷新。Java具有用于此類通知的標(biāo)準(zhǔn)機制:PropertyChangeSupport和PropertyChangeEvent類。讓我們看看在Kotlin中不使用委托屬性的情況下,該如何使用它們,然后我們再將代碼重構(gòu)為用委托屬性的方式。
PropertyChangeSupport類維護了一個監(jiān)聽器列表,并向它們發(fā)送PropertyChangeEvent事件。要使用它,你通常需要把這個類的一個實例存儲為bean類的一個字段,并將屬性更改的處理委托給它。
為了避免要在每個類中添加這個字段,你需要創(chuàng)建一個小的工具類,用來存儲PropertyChangeSupport的實例并監(jiān)聽屬性更改。之后,你的類會繼承這個工具類,以訪問changeSupport。
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
現(xiàn)在我們來寫一個Person類,定義一個只讀屬性(作為一個人的名字,一般不會隨時更改)和兩個可寫屬性:年齡和工資。當(dāng)這個人的年齡或工資發(fā)生變化時,這個類將通知它的監(jiān)聽器。
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
val oldValue = field //field標(biāo)識符訪問支持字段
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue) //屬性變化時通知監(jiān)聽器
}
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)
//添加監(jiān)聽器
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中有很多重復(fù)的代碼,我們來嘗試提取一個類,用來存儲這個屬性的值并發(fā)起通知。
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)
}
現(xiàn)在,你應(yīng)該已經(jīng)差不多理解了在Kotlin中,委托屬性是如何工作的。你創(chuàng)建了一個保存屬性值的類,并在修改屬性時自動觸發(fā)更改通知。你刪除了重復(fù)的邏輯代碼,但是需要相當(dāng)多的樣板代碼來為每個屬性創(chuàng)建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)
}
}
與之前的版本相比,這次代碼做了一些更改:
- 現(xiàn)在,按照也回到那個的需要,getValue和setValue函數(shù)被標(biāo)記了operator
- 這些函數(shù)加了兩個參數(shù):一個用于接收屬性的實例,用來設(shè)置或讀取屬性,另一個用于表示屬性本身。這個屬性類型為KProperty(之后章節(jié)會詳細(xì)介紹它),現(xiàn)在你只需要知道可以通過KProperty.name的方式來訪問該屬性的名稱。
- 把name屬性從主構(gòu)造方法中刪除了,因為現(xiàn)在已經(jīng)可以通過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)
}
通過關(guān)鍵字by,Kotlin編譯器會自動執(zhí)行之前版本的代碼中手動完成的操作。如果把這份代碼與之前版本的Person類進行比較:使用委托屬性時生成的代碼非常類似,右邊的對象被稱為委托。Kotlin會自動將委托存儲在隱藏的屬性中,并在訪問或修改屬性時調(diào)用委托的getValue和setValue。
你不用手動去實現(xiàn)可觀察的屬性邏輯,可以使用Kotlin標(biāo)準(zhǔn)庫,它已經(jīng)包含了類似ObserverProperty的類。標(biāo)準(zhǔn)庫和這里使用的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右邊的表達(dá)式不一定是新創(chuàng)建的實例,也可以是函數(shù)調(diào)用,另一個屬性或任何其他表達(dá)式,只要這個表達(dá)式的值,是能夠被編譯器用正確的參數(shù)類型來調(diào)用getValue和setValue的對象。與其他約定一樣,getValue和setValue可以是對象自己生命的方法或擴展函數(shù)。
注意,為了讓示例保持簡單,我們只展示了如何使用類型為Int的委托屬性,委托屬性機制其實是通用的,適用于任何其他類型。
委托屬性的變換規(guī)則
讓我們來總結(jié)一下委托屬性是怎樣工作的,假設(shè)你已經(jīng)有了一個具有委托屬性的類:
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)
}
因此,在每個屬性訪問器中,編譯器都會生成對應(yīng)的getValue和setValue方法:
val x = c.prop -> val x = <delegate>.getValue(c, <property>)
c,prop = x -> <delegate>.setValue(c, <property>, x)
這個機制非常簡單,但它可以實現(xiàn)許多有趣的場景。你可以自定義存儲該屬性值得位置(map、數(shù)據(jù)庫表或者用戶會話的Cookie中),以及在訪問該屬性時做點什么(比如添加驗證、更改通知等)。
在map中保存屬性值
委托屬性發(fā)揮作用的另一種常見用法,是用在有動態(tài)定義的屬性集的對象中。這樣的對象有時候被稱為自定(expando)對象。例如,考慮一個聯(lián)系人管理系統(tǒng),可以用來存儲有關(guān)聯(lián)系人的任意信息。系統(tǒng)中的每個人都有一些屬性需要特殊處理(例如名字),以及每個人特有的數(shù)量任意的額外屬性(例如,最小的孩子的生日)。
實現(xiàn)這種系統(tǒng)的一種方法是將人的所有屬性存儲在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來吧數(shù)據(jù)加載到對象中(在實際項目中,可以是JSON反序列化或類似的方法),然后使用特定的API來訪問一個屬性的值。把它改為委托屬性非常簡單,可以直接將map放在by關(guān)鍵字后面。
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
因為標(biāo)準(zhǔn)庫已經(jīng)在標(biāo)準(zhǔn)Map和MutableMap接口上定義了getValue和setValue擴展函數(shù),所以這里可以直接這樣用。屬性的名稱將自動用作map中的鍵,屬性值作為map中的值。改動前p.name隱藏了_attributes.getValue(p, prop)的調(diào)用,改動后變?yōu)?code>_attributes[prop.name]。