一、本文概要
本文是對<<Kotlin in Action>>
的學習筆記,如果需要執行相應的程式碼可以訪問線上環境 try.kotlinlang.org,這部分的思維導圖為:
二、委託屬性的基本操作
2.1 委託屬性的基本語法
class Foo {
var p : Type by Delegate()
}
複製程式碼
型別為Type
的屬性p
將它的訪問器邏輯委託給了另一個Delegate
例項,通過關鍵字by
對其後的 表示式求值 來獲取這個物件,關鍵字by
可以用於任何 符合屬性委託約定規則的物件。
按照約定,Delegate
類必須具有getValue
和setValue
方法,它們可以是成員函式,也可以是擴充套件函式,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()"
惰性初始化是一種常見的模式,直到 在第一次訪問該屬性 的時候,才根據需要建立物件的一部分。
2.2.1 使用支援屬性來實現惰性初始化
使用這種技術來實現惰性初始化時,需要兩個值,一個是對內部可見的可空_emails
變數,另一個是提供對屬性的讀取訪問的email
變數,它是非空的,在email
的get()
函式中首先判斷_emails
變數是否為空,如果為空那麼就先初始化它,否則直接返回。
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
具有用於此類通知的標準機制:PropertyChangeSupport
和PropertyChangeEvent
。PropertyChangeSupport
類維護了一個監聽器列表,並向它們傳送PropertyChangeEvent
事件,要使用它,你通常需要把PropertyChangeSupport
的一個例項儲存為bean
類的一個欄位,並將屬性更改的處理委託給它。
為了避免在每個類中去新增這個欄位,你需要建立一個小的工具類,用來儲存PropertyChangeSupport
的例項並監聽屬性更改,之後,你的類會繼承這個工具類,以訪問changeSupport
。
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
的例項,並把getter
和setter
委託給它,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
相比,我們做了以下幾點修改:
- 按照約定的需要,
getValue
和setValue
函式被標記了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)
}
}
複製程式碼
因此,在每個屬性訪問器中,編譯器都會生成對應的getValue
和setValue
方法。
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)
}
複製程式碼
因為標準庫已經在標準map
和MutableMap
介面上定義了getValue
和setValue
擴充套件函式,所以可以在這裡直接呼叫。
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/