Kotlin 知識梳理(9) 委託屬性

1、本文概要

本文是對<<Kotlin in Action>>的學習筆記,若是須要運行相應的代碼能夠訪問在線環境 try.kotlinlang.org,這部分的思惟導圖爲: java

2、委託屬性的基本操做

2.1 委託屬性的基本語法

class Foo {
    var p : Type by Delegate() } 複製代碼

類型爲Type的屬性p將它的訪問器邏輯委託給了另外一個Delegate實例,經過關鍵字by對其後的 表達式求值 來獲取這個對象,關鍵字by能夠用於任何 符合屬性委託約定規則的對象安全

按照約定,Delegate類必須具備getValuesetValue方法,它們能夠是成員函數,也能夠是擴展函數,Delegate的簡單實現以下:函數

class Delegate {
    operator fun getValue(...) { ... }
    operator fun setValue(..., value : Type) { ... }
}
複製代碼

使用方法以下:工具

val foo = Foo()
val oldValue = foo.p
foo.p = newValue
複製代碼

當咱們將foo.p做爲普通屬性使用時,實際上將調用Delegate類型的輔助屬性的方法。爲了研究這種機制如何在實踐中使用,咱們首先看一個委託屬性展現威力的例子:庫對惰性初始化的支持學習

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

惰性初始化是一種常見的模式,直到 在第一次訪問該屬性 的時候,才根據須要建立對象的一部分。this

2.2.1 使用支持屬性來實現惰性初始化

使用這種技術來實現惰性初始化時,須要兩個值,一個是對內部可見的可空_emails變量,另外一個是提供對屬性的讀取訪問的email變量,它是非空的,在emailget()函數中首先判斷_emails變量是否爲空,若是爲空那麼就先初始化它,不然直接返回。spa

2.2.2 使用委託屬性來實現惰性初始化

class Person(val name : String) {
    val emails by lazy { loadEmails(this) }
}
複製代碼

這裏可使用標準庫函數lazy返回的委託,lazy函數返回一個對象,該對象具備一個名爲getValue且簽名正確的方法,所以能夠把它與by關鍵字一塊兒使用來建立一個委託屬性。lazy的參數是一個lambda,能夠調用它來初始化這個值,默認狀況下,lazy函數是線程安全的。線程

2.3 實現委託屬性

2.3.1 常規實現方式

要了解委託屬性的實現方式,讓咱們來看另外一個例子:當一個對象的屬性更改時通知監聽器。Java具備用於此類通知的標準機制:PropertyChangeSupportPropertyChangeEventPropertyChangeSupport類維護了一個監聽器列表,並向它們發送PropertyChangeEvent事件,要使用它,你一般須要把PropertyChangeSupport的一個實例存儲爲bean類的一個字段,並將屬性更改的處理委託給它。code

爲了不在每一個類中去添加這個字段,你須要建立一個小的工具類,用來存儲PropertyChangeSupport的實例並監聽屬性更改,以後,你的類會繼承這個工具類,以訪問changeSupportcdn

open class ValueChangeAware {   
    protected val changeSupport = PropertyChangeSupport(this)
    //添加監聽者。
    fun addListener(listener : PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }
    //移除監聽者。
    fun removeListener(listener : PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

//輔助類,若是經過該輔助類改變了屬性,那麼將會通知監聽者。
class ObservableValue (
    val valueName : String, var valueValue : Int, 
    val changeSupport : PropertyChangeSupport
) {
    fun getValue() : Int = valueValue
    fun setValue(newValue : Int) {
        val oldValue = valueValue
        valueValue = newValue
        //通知監聽者。
        changeSupport.firePropertyChange(valueName, oldValue, newValue)
    }
}

class Person(val name : String, age : Int) : ValueChangeAware() {
    // _age 爲輔助類的一個實例。
    val _age = ObservableValue("age", age, changeSupport)
    //經過輔助類進行讀寫操做。
    var age : Int
    	get() = _age.getValue()
    	set(value) { _age.setValue(value) }
}
複製代碼

下面是實際應用的代碼:

fun main(args: Array<String>) {
	val person = Person("zemao", 20)
    person.addListener(
        //監聽者打印出改變的屬性名、原屬性值和新的屬性值。
    	PropertyChangeListener { event ->                              
        	println("${event.propertyName} " + 
                    "changed from ${event.oldValue} to ${event.newValue}" )
        }
    )
    person.age = 18
}
複製代碼

運行結果爲:

>> age changed from 20 to 18
複製代碼

2.3.2 使用 ObservableValue 做爲屬性委託

在上面的代碼中,若是Person類中包含了多個與age相似的屬性,那麼就須要建立多個_age的實例,並把gettersetter委託給它,Kotlin的委託屬性可讓你擺脫這些樣板代碼,首先,咱們須要重寫ObservableValue代碼,讓它符合屬性委託的約定。

class ObservableValue (
	var valueValue : Int, 
    val changeSupport : PropertyChangeSupport
) {
    //按照約定的須要,用 operator 來標記,並添加了 KProperty。
    operator fun getValue(p : Person, prop : KProperty<*>) : Int = valueValue
    operator fun setValue(p : Person, prop : KProperty<*>, newValue : Int) {
        val oldValue = valueValue
        valueValue = newValue
        //通知監聽者。
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}
複製代碼

2.3.1相比,咱們作了如下幾點修改:

  • 按照約定的須要,getValuesetValue函數被標記了operator
  • 這些函數加了兩個參數:一個用於接收屬性的實例,用來設置和讀取屬性,另外一個用於表示屬性自己,這個屬性類型爲KProperty,你可使用KProperty.name的方式來訪問該屬性的名稱。

下面,咱們再修改Person類,將age屬性委託給ObservableValue類:

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

運行結果和2.3.1相同。

2.3.3 使用 Delegates.observable 來實現屬性修改的通知

Kotlin標準庫中,已經包含了相似於ObservableValue的類,所以咱們不用手動去實現可觀察的屬性邏輯,下面咱們重寫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)
   
}
複製代碼

運行結果和以上兩小結相同。

2.4 委託屬性的變換規則

讓咱們來總結一下委託屬性是怎麼工做的,假設你已經有了一個具備委託屬性的類:

class Foo {
    var p : Type by Delegate() } 複製代碼

Delegate實例將會被保存到一個隱藏的屬性中,它被稱爲<delegate>,編譯器也將用一個KProperty類型的對象來表示這個屬性,它被稱爲<property>,編譯器生成的的代碼以下:

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

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

所以,在每一個屬性訪問器中,編譯器都會生成對應的getValuesetValue方法。

2.5 在 map 中保存屬性的值

委託屬性發揮做用的另外一種常見用法是 用在動態定義的屬性集的對象中,這樣的對象有時被稱爲 自訂對象。例如考慮一個聯繫人管理系統,能夠用來存儲有關聯繫人的任意信息,系統中的每一個人都有一些屬性須要特殊處理(例如名字),以及每一個人特有的數量任意的額外屬性(例如,最小的孩子的生日)。

實現這種系統的一種方法是將人的全部屬性存儲在map中,不肯定提供屬性,來訪問須要特殊處理的信息。

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)
}
複製代碼

由於標準庫已經在標準mapMutableMap接口上定義了getValuesetValue擴展函數,因此能夠在這裏直接調用。


更多文章,歡迎訪問個人 Android 知識梳理系列:

相關文章
相關標籤/搜索