Kotlin 知識(shí)梳理(9) - 委托屬性

Kotlin 知識(shí)梳理系列文章

Kotlin 知識(shí)梳理(1) - Kotlin 基礎(chǔ)
Kotlin 知識(shí)梳理(2) - 函數(shù)的定義與調(diào)用
Kotlin 知識(shí)梳理(3) - 類佛点、對(duì)象和接口
Kotlin 知識(shí)梳理(4) - 數(shù)據(jù)類伪窖、類委托 及 object 關(guān)鍵字
Kotlin 知識(shí)梳理(5) - lambda 表達(dá)式和成員引用
Kotlin 知識(shí)梳理(6) - Kotlin 的可空性
Kotlin 知識(shí)梳理(7) - Kotlin 的類型系統(tǒng)
Kotlin 知識(shí)梳理(8) - 運(yùn)算符重載及其他約定
Kotlin 知識(shí)梳理(9) - 委托屬性
Kotlin 知識(shí)梳理(10) - 高階函數(shù):Lambda 作為形參或返回值
Kotlin 知識(shí)梳理(11) - 內(nèi)聯(lián)函數(shù)
Kotlin 知識(shí)梳理(12) - 泛型類型參數(shù)


一、本文概要

本文是對(duì)<<Kotlin in Action>>的學(xué)習(xí)筆記第献,如果需要運(yùn)行相應(yīng)的代碼可以訪問(wèn)在線環(huán)境 try.kotlinlang.org囊拜,這部分的思維導(dǎo)圖為:

二、委托屬性的基本操作

2.1 委托屬性的基本語(yǔ)法

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

類型為Type的屬性p將它的訪問(wèn)器邏輯委托給了另一個(gè)Delegate實(shí)例究孕,通過(guò)關(guān)鍵字by對(duì)其后的 表達(dá)式求值 來(lái)獲取這個(gè)對(duì)象啥酱,關(guān)鍵字by可以用于任何 符合屬性委托約定規(guī)則的對(duì)象

按照約定厨诸,Delegate類必須具有getValuesetValue方法镶殷,它們可以是成員函數(shù),也可以是擴(kuò)展函數(shù)微酬,Delegate的簡(jiǎn)單實(shí)現(xiàn)如下:

class Delegate {
    operator fun getValue(...) { ... }
    operator fun setValue(..., value : Type) { ... }
}

使用方法如下:

val foo = Foo()
val oldValue = foo.p
foo.p = newValue

當(dāng)我們將foo.p作為普通屬性使用時(shí)绘趋,實(shí)際上將調(diào)用Delegate類型的輔助屬性的方法。為了研究這種機(jī)制如何在實(shí)踐中使用颗管,我們首先看一個(gè)委托屬性展示威力的例子:庫(kù)對(duì)惰性初始化的支持陷遮。

2.2 使用委托屬性:惰性初始化和 "by lazy()"

惰性初始化是一種常見(jiàn)的模式,直到 在第一次訪問(wèn)該屬性 的時(shí)候忙上,才根據(jù)需要?jiǎng)?chuàng)建對(duì)象的一部分拷呆。

2.2.1 使用支持屬性來(lái)實(shí)現(xiàn)惰性初始化

使用這種技術(shù)來(lái)實(shí)現(xiàn)惰性初始化時(shí),需要兩個(gè)值疫粥,一個(gè)是對(duì)內(nèi)部可見(jiàn)的可空_emails變量茬斧,另一個(gè)是提供對(duì)屬性的讀取訪問(wèn)的email變量,它是非空的梗逮,在emailget()函數(shù)中首先判斷_emails變量是否為空项秉,如果為空那么就先初始化它,否則直接返回慷彤。

2.2.2 使用委托屬性來(lái)實(shí)現(xiàn)惰性初始化

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

這里可以使用標(biāo)準(zhǔn)庫(kù)函數(shù)lazy返回的委托娄蔼,lazy函數(shù)返回一個(gè)對(duì)象,該對(duì)象具有一個(gè)名為getValue且簽名正確的方法底哗,因此可以把它與by關(guān)鍵字一起使用來(lái)創(chuàng)建一個(gè)委托屬性岁诉。lazy的參數(shù)是一個(gè)lambda,可以調(diào)用它來(lái)初始化這個(gè)值跋选,默認(rèn)情況下涕癣,lazy函數(shù)是線程安全的。

2.3 實(shí)現(xiàn)委托屬性

2.3.1 常規(guī)實(shí)現(xiàn)方式

要了解委托屬性的實(shí)現(xiàn)方式前标,讓我們來(lái)看另一個(gè)例子:當(dāng)一個(gè)對(duì)象的屬性更改時(shí)通知監(jiān)聽(tīng)器坠韩。Java具有用于此類通知的標(biāo)準(zhǔn)機(jī)制:PropertyChangeSupportPropertyChangeEvent距潘。PropertyChangeSupport類維護(hù)了一個(gè)監(jiān)聽(tīng)器列表,并向它們發(fā)送PropertyChangeEvent事件只搁,要使用它音比,你通常需要把PropertyChangeSupport的一個(gè)實(shí)例存儲(chǔ)為bean類的一個(gè)字段,并將屬性更改的處理委托給它氢惋。

為了避免在每個(gè)類中去添加這個(gè)字段洞翩,你需要?jiǎng)?chuàng)建一個(gè)小的工具類,用來(lái)存儲(chǔ)PropertyChangeSupport的實(shí)例并監(jiān)聽(tīng)屬性更改明肮,之后菱农,你的類會(huì)繼承這個(gè)工具類,以訪問(wèn)changeSupport柿估。

open class ValueChangeAware {   
    protected val changeSupport = PropertyChangeSupport(this)
    //添加監(jiān)聽(tīng)者循未。
    fun addListener(listener : PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }
    //移除監(jiān)聽(tīng)者。
    fun removeListener(listener : PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

//輔助類秫舌,如果通過(guò)該輔助類改變了屬性的妖,那么將會(huì)通知監(jiān)聽(tīng)者。
class ObservableValue (
    val valueName : String, var valueValue : Int, 
    val changeSupport : PropertyChangeSupport
) {
    fun getValue() : Int = valueValue
    fun setValue(newValue : Int) {
        val oldValue = valueValue
        valueValue = newValue
        //通知監(jiān)聽(tīng)者足陨。
        changeSupport.firePropertyChange(valueName, oldValue, newValue)
    }
}

class Person(val name : String, age : Int) : ValueChangeAware() {
    // _age 為輔助類的一個(gè)實(shí)例嫂粟。
    val _age = ObservableValue("age", age, changeSupport)
    //通過(guò)輔助類進(jìn)行讀寫操作。
    var age : Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }
}

下面是實(shí)際應(yīng)用的代碼:

fun main(args: Array<String>) {
    val person = Person("zemao", 20)
    person.addListener(
        //監(jiān)聽(tīng)者打印出改變的屬性名墨缘、原屬性值和新的屬性值星虹。
        PropertyChangeListener { event ->                              
            println("${event.propertyName} " + 
                    "changed from ${event.oldValue} to ${event.newValue}" )
        }
    )
    person.age = 18
}

運(yùn)行結(jié)果為:

>> age changed from 20 to 18

2.3.2 使用 ObservableValue 作為屬性委托

在上面的代碼中,如果Person類中包含了多個(gè)與age類似的屬性镊讼,那么就需要?jiǎng)?chuàng)建多個(gè)_age的實(shí)例宽涌,并把gettersetter委托給它,Kotlin的委托屬性可以讓你擺脫這些樣板代碼蝶棋,首先卸亮,我們需要重寫ObservableValue代碼,讓它符合屬性委托的約定玩裙。

class ObservableValue (
    var valueValue : Int, 
    val changeSupport : PropertyChangeSupport
) {
    //按照約定的需要兼贸,用 operator 來(lái)標(biāo)記,并添加了 KProperty吃溅。
    operator fun getValue(p : Person, prop : KProperty<*>) : Int = valueValue
    operator fun setValue(p : Person, prop : KProperty<*>, newValue : Int) {
        val oldValue = valueValue
        valueValue = newValue
        //通知監(jiān)聽(tīng)者溶诞。
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

2.3.1相比,我們做了以下幾點(diǎn)修改:

  • 按照約定的需要决侈,getValuesetValue函數(shù)被標(biāo)記了operator很澄。
  • 這些函數(shù)加了兩個(gè)參數(shù):一個(gè)用于接收屬性的實(shí)例,用來(lái)設(shè)置和讀取屬性,另一個(gè)用于表示屬性本身甩苛,這個(gè)屬性類型為KProperty,你可以使用KProperty.name的方式來(lái)訪問(wèn)該屬性的名稱俏站。

下面讯蒲,我們?cè)傩薷?code>Person類,將age屬性委托給ObservableValue類:

class Person(val name : String, age : Int) : ValueChangeAware() {
    var age : Int by ObservableValue(age, changeSupport)
}

運(yùn)行結(jié)果和2.3.1相同肄扎。

2.3.3 使用 Delegates.observable 來(lái)實(shí)現(xiàn)屬性修改的通知

Kotlin標(biāo)準(zhǔn)庫(kù)中墨林,已經(jīng)包含了類似于ObservableValue的類,因此我們不用手動(dòng)去實(shí)現(xiàn)可觀察的屬性邏輯犯祠,下面我們重寫Person類:

class Person(
    val name: String, age: Int
) : ValueChangeAware() {

    private val observer = {
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

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

運(yùn)行結(jié)果和以上兩小結(jié)相同旭等。

2.4 委托屬性的變換規(guī)則

讓我們來(lái)總結(jié)一下委托屬性是怎么工作的,假設(shè)你已經(jīng)有了一個(gè)具有委托屬性的類:

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

Delegate實(shí)例將會(huì)被保存到一個(gè)隱藏的屬性中衡载,它被稱為<delegate>搔耕,編譯器也將用一個(gè)KProperty類型的對(duì)象來(lái)表示這個(gè)屬性,它被稱為<property>痰娱,編譯器生成的的代碼如下:

class Foo {
    private val <delegate> = Delegate()

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

因此弃榨,在每個(gè)屬性訪問(wèn)器中,編譯器都會(huì)生成對(duì)應(yīng)的getValuesetValue方法梨睁。

2.5 在 map 中保存屬性的值

委托屬性發(fā)揮作用的另一種常見(jiàn)用法是 用在動(dòng)態(tài)定義的屬性集的對(duì)象中鲸睛,這樣的對(duì)象有時(shí)被稱為 自訂對(duì)象。例如考慮一個(gè)聯(lián)系人管理系統(tǒng)坡贺,可以用來(lái)存儲(chǔ)有關(guān)聯(lián)系人的任意信息官辈,系統(tǒng)中的每個(gè)人都有一些屬性需要特殊處理(例如名字),以及每個(gè)人特有的數(shù)量任意的額外屬性(例如遍坟,最小的孩子的生日)拳亿。

實(shí)現(xiàn)這種系統(tǒng)的一種方法是將人的所有屬性存儲(chǔ)在map中,不確定提供屬性政鼠,來(lái)訪問(wèn)需要特殊處理的信息风瘦。

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    //把 map 作為委托屬性。
    val name: String by _attributes
}

使用方式:

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

因?yàn)闃?biāo)準(zhǔn)庫(kù)已經(jīng)在標(biāo)準(zhǔn)mapMutableMap接口上定義了getValuesetValue擴(kuò)展函數(shù)公般,所以可以在這里直接調(diào)用万搔。


更多文章,歡迎訪問(wèn)我的 Android 知識(shí)梳理系列:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末官帘,一起剝皮案震驚了整個(gè)濱河市瞬雹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刽虹,老刑警劉巖酗捌,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡胖缤,警方通過(guò)查閱死者的電腦和手機(jī)尚镰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)哪廓,“玉大人狗唉,你說(shuō)我怎么就攤上這事∥姓妫” “怎么了分俯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)哆料。 經(jīng)常有香客問(wèn)我缸剪,道長(zhǎng),這世上最難降的妖魔是什么东亦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任杏节,我火速辦了婚禮,結(jié)果婚禮上讥此,老公的妹妹穿的比我還像新娘拢锹。我一直安慰自己,他們只是感情好萄喳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布卒稳。 她就那樣靜靜地躺著,像睡著了一般他巨。 火紅的嫁衣襯著肌膚如雪充坑。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天染突,我揣著相機(jī)與錄音捻爷,去河邊找鬼。 笑死份企,一個(gè)胖子當(dāng)著我的面吹牛也榄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播司志,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼甜紫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了骂远?” 一聲冷哼從身側(cè)響起囚霸,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎激才,沒(méi)想到半個(gè)月后拓型,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體额嘿,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年劣挫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了册养。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡压固,死狀恐怖捕儒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邓夕,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布阎毅,位于F島的核電站焚刚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扇调。R本人自食惡果不足惜矿咕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狼钮。 院中可真熱鬧碳柱,春花似錦、人聲如沸熬芜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涎拉。三九已至瑞侮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鼓拧,已是汗流浹背半火。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留季俩,地道東北人钮糖。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像酌住,于是被迫代替她去往敵國(guó)和親店归。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容