如何使用 Delegated Properties

如何使用 Delegated Properties

委託屬性從字面上的意思就是拜託別人幫忙,幫忙什麼?你可以透過延遲的方式來初始化一個屬性,也可以透過該變數有變化的時候進行通知,更可以透過多個屬性一起初始化,這邊就是要透過 Delegated Properties 這個內建好用又強大的方式來進行。

說明

在 Kotlin 上面你可以透過關鍵字 by 來進行委託,通常這類的委託就是我不自己做,我請別人做的意思,所以你可以透過這樣去寫程式。

class Delegate{}

class Example {
    var p: String by Delegate()
}

如果你只有寫這樣,那麼你會得到以下訊息。

Type 'Delegate' has no method 'getValue(Example, KProperty<*>)' and thus it cannot serve as a delegate

它的意思就是要你設定好 getValue 才可以使用 Delegate。

所以我們設定好 getValue。

class Delegate {
    private var data: String = "init"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return data
    }
}

但是如果只設定好 getValue 是不夠的,你會看到又顯示以下訊息。

Type 'Delegate' has no method 'setValue(Example, KProperty<*>, String)' and thus it cannot serve as a delegate for var (read-write property)

原來還要重寫 setValue 這個方法,所以我們就再設定一下 setValue。

class Delegate {
    private var data: String = "init"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return data
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        data = value
    }
}

為什麼要覆寫這兩個方法呢?其實原理很簡單,因為我們是委託屬性,而屬性只會有兩種情況,一種是設定另外一種是取得,也就是 getter/setter,因此,你透過 by 這個關鍵字去進行委託別的類別幫忙,你就必須表示出 getter/setter 是怎麼實作的,那麼 by 為什麼知道要實作 getValue/setValue,這是因為 Kotlin 的標準函式庫裡面有一個介面 ReadWriteProperty ,在 Delegates 中,就必須實作這兩個方法。

/**
 * Base interface that can be used for implementing property delegates of read-write properties.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your property delegate has methods with the same signatures.
 *
 * @param R the type of object which owns the delegated property.
 * @param T the type of the property value.
 */
public interface ReadWriteProperty<in R, T> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public operator fun getValue(thisRef: R, property: KProperty<*>): T

    /**
     * Sets the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @param value the value to set.
     */
    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

所以透過這樣的方式,就可以來進行委託,所以當我們實作完畢以後就可以如下方所示。

class Delegate {
    private var data: String = "init"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return data
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        data = value
    }
}

class Example {
    var p: String by Delegate()
}

fun main() {
    val e = Example()
    println(e.p)
    e.p = "abc"
    println(e.p)
}

你會說這樣跟 getter/setter 有什麼不一樣?其實就是透過 by 我們將初始化的工作交付給 Delegate 類別去幫忙處理 getter/setter,那你會想說我們為什麼要把 getter/setter?

其實原因很簡單,如果今天有一個屬性是許多類別需要的,最簡單的方法就是每個類別都寫這個屬性,並且每個類別都初始化這個屬性,這樣一來,就會變成一種浪費,因為每個類別都寫相同屬性不但程式會變得很多餘,而且一旦有一天需要修改的時候,你會不知道還有哪幾個類別是需要一起修改,而修改就會造成邊際效應。

因此,就會有人提出那不如寫在父類別,這就會產生另外一個問題了,如果初始化的屬性其實多個屬性,最好的方式反而不是寫在類別,或許他只是一個功能上的屬性,那麼就比較不適合用在繼承。

Kotlin 支持了 Delegated propeties,透過這樣的方式,你就可以透過關鍵字 by 來輕鬆整理在同一個類別。

標準委託

lazy

我們知道如果宣告一個變數,那麼我們要嘛就是設定好初始值,要不然一開始就設定好 null,但是又只能在可變的變數上進行,如果你想要在不可變動的變數 (val宣告的) 上進行延遲初始化,你可以透過 by lazy 的方式來進行來進行。

val name: String by lazy {
    "givemepass"
}
fun main() {
    println(name)
}

結果就會印出字串內容。

givemepass

lazy 是內建的一個方法,點進去看可以看到以下原始碼。

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

你會發現這是一個執行緒安全的內建函式,透過這樣的方式,我們可以放心的初始化它,如果你不需要使用 synchronized,那麼就可以透過 LazyThreadSafetyMode.NONE 來解除執行緒安全的一切開銷。

val name: String by lazy(LazyThreadSafetyMode.NONE) {
    "givemepass"
}

可以參考 LazyThreadSafetyMode 其他屬性值。

Observable

var name: String by Delegates.observable("test") { _, old, new ->
    println("new:$new, old:$old")
}

fun main() {
    name = "givemepass1"
    name = "givemepass2"
}

這樣一來只要屬性有變化,我們就會收到對應的訊息,結果如下。

new:givemepass1, old:test
new:givemepass2, old:givemepass1

Storing Properties in a Map

儲存在 Map 的屬性,可以透過 Map 來進行初始化,有時候你會想要把對應的 Map key/value 直接儲存在對應的屬性跟值,這時候就可以透過這樣的方式來進行初始化。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

fun main() {
    val user = User(mapOf("name" to "givemepass", "age" to 30))
    println("name:${user.name}, age:${user.age}")
}

這樣印出來的結果如下。

name:givemepass, age:30

參考資料

https://kotlinlang.org/docs/reference/delegated-properties.html