Kotlin委託屬性

有一些常見的屬性類型,雖然我們可以在每次需要的時候手動實現它們,
但是如果能夠爲大家把他們只實現一次並放入一個庫會更好。例如包括

  • 延遲屬性(lazy properties): 其值只在首次訪問時計算,
  • 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知,
  • 把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。

爲了涵蓋這些(以及其他)情況,Kotlin 支持 委託屬性:

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

語法是: val/var <屬性名>: <類型> by <表達式>。在 by{:.keyword} 後面的表達式是該 委託
因爲屬性對應的 get()(和 set())會被委託給它的 getValue()setValue() 方法。
屬性的委託不必實現任何的接口,但是需要提供一個 getValue() 函數(和 setValue()——對於 var{:.keyword} 屬性)。
例如:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

當我們從委託到一個 Delegate 實例的 p 讀取時,將調用 Delegate 中的 getValue() 函數,
所以它第一個參數是讀出 p 的對象、第二個參數保存了對 p 自身的描述
(例如你可以取它的名字)。 例如:

val e = Example()
println(e.p)

輸出結果:

Example@33a17727, thank you for delegating ‘p’ to me!

類似地,當我們給 p 賦值時,將調用 setValue() 函數。前兩個參數相同,第三個參數保存將要被賦予的值:

e.p = "NEW"

輸出結果:

NEW has been assigned to ‘p’ in Example@33a17727.

委託對象的要求規範可以在下文找到。

請注意,自 Kotlin 1.1 起你可以在函數或代碼塊中聲明一個委託屬性,因此它不一定是類的成員。
你可以在下文找到其示例)。

標準委託

Kotlin 標準庫爲幾種有用的委託提供了工廠方法。

延遲屬性 Lazy

lazy() 是接受一個 lambda 並返回一個 Lazy <T> 實例的函數,返回的實例可以作爲實現延遲屬性的委託:
第一次調用 get() 會執行已傳遞給 lazy() 的 lamda 表達式並記錄結果,
後續調用 get() 只是返回記錄的結果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

這個例子輸出:

computed!
Hello
Hello

默認情況下,對於 lazy 屬性的求值是**同步鎖的(synchronized)**:該值只在一個線程中計算,並且所有線程
會看到相同的值。如果初始化委託的同步鎖不是必需的,這樣多個線程
可以同時執行,那麼將 LazyThreadSafetyMode.PUBLICATION 作爲參數傳遞給 lazy() 函數。
而如果你確定初始化將總是發生在單個線程,那麼你可以使用 LazyThreadSafetyMode.NONE 模式,
它不會有任何線程安全的保證和相關的開銷。

可觀察屬性 Observable

Delegates.observable() 接受兩個參數:初始值和修改時處理程序(handler)。
每當我們給屬性賦值時會調用該處理程序(在賦值執行)。它有三個
參數:被賦值的屬性、舊值和新值:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

這個例子輸出:

<no name> -> first
first -> second

如果你想能夠截獲一個賦值並「否決」它,就使用 vetoable() 取代 observable()
在屬性被賦新值生效之前會調用傳遞給 vetoable 的處理程序。

把屬性儲存在映射中

一個常見的用例是在一個映射(map)裏存儲屬性的值。
這經常出現在像解析 JSON 或者做其他「動態」事情的應用中。
在這種情況下,你可以使用映射實例自身作爲委託來實現委託屬性。

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

在這個例子中,構造函數接受一個映射參數:

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

委託屬性會從這個映射中取值(通過字符串鍵——屬性的名稱):

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

這也適用於 var{:.keyword} 屬性,如果把只讀的 Map 換成 MutableMap 的話:

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

局部委託屬性(自 1.1 起)

你可以將局部變量聲明爲委託屬性。
例如,你可以使一個局部變量惰性初始化:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

memoizedFoo 變量只會在第一次訪問時計算。
如果 someCondition 失敗,那麼該變量根本不會計算。

屬性委託要求

這裏我們總結了委託對象的要求。

對於一個只讀屬性(即 val{:.keyword} 聲明的),委託必須提供一個名爲 getValue 的函數,該函數接受以下參數:

  • thisRef —— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是它的超類型,
  • property —— 必須是類型 KProperty<*> 或其超類型,

這個函數必須返回與屬性相同的類型(或其子類型)。

對於一個可變屬性(即 var{:.keyword} 聲明的),委託必須額外提供一個名爲 setValue 的函數,該函數接受以下參數:

  • thisRef —— 同 getValue()
  • property —— 同 getValue()
  • new value —— 必須和屬性同類型或者是它的超類型。

getValue() 或/和 setValue() 函數可以通過委託類的成員函數提供或者由擴展函數提供。
當你需要委託屬性到原本未提供的這些函數的對象時後者會更便利。
兩函數都需要用 operator 關鍵字來進行標記。

委託類可以實現包含所需 operator 方法的 ReadOnlyPropertyReadWriteProperty 接口之一。
這倆接口是在 Kotlin 標準庫中聲明的:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

翻譯規則

在每個委託屬性的實現的背後,Kotlin 編譯器都會生成輔助屬性並委託給它。
例如,對於屬性 prop,生成隱藏屬性 prop$delegate,而訪問器的代碼只是簡單地委託給這個附加屬性:

class C {
    var prop: Type by MyDelegate()
}

// 這段是由編譯器生成的相應代碼:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Kotlin 編譯器在參數中提供了關於 prop 的所有必要信息:第一個參數 this 引用到外部類 C 的實例而 this::propKProperty 類型的反射對象,該對象描述 prop 自身。

請注意,直接在代碼中引用綁定的可調用引用)的語法 this::prop 自 Kotlin 1.1 起纔可用。

提供委託(自 1.1 起)

通過定義 provideDelegate 操作符,可以擴展創建屬性實現所委託對象的邏輯。
如果 by 右側所使用的對象將 provideDelegate 定義爲成員或擴展函數,那麼會調用該函數來
創建屬性委託實例。

provideDelegate 的一個可能的使用場景是在創建屬性時(而不僅在其 getter 或 setter 中)檢查屬性一致性。

例如,如果要在綁定之前檢查屬性名稱,可以這樣寫:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 創建委託
    }

    private fun checkProperty(thisRef: MyUI, name: String) { …… }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate 的參數與 getValue 相同:

  • thisRef —— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是它的超類型,
  • property —— 必須是類型 KProperty<*> 或其超類型。

在創建 MyUI 實例期間,爲每個屬性調用 provideDelegate 方法,並立即執行必要的驗證。

如果沒有這種攔截屬性與其委託之間的綁定的能力,爲了實現相同的功能,
你必須顯式傳遞屬性名,這不是很方便:

// 檢查屬性名稱而不使用「provideDelegate」功能
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
   checkProperty(this, propertyName)
   // 創建委託
}

在生成的代碼中,會調用 provideDelegate 方法來初始化輔助的 prop$delegate 屬性。
比較對於屬性聲明 val prop: Type by MyDelegate() 生成的代碼與
上面(當 provideDelegate 方法不存在時)生成的代碼:

class C {
    var prop: Type by MyDelegate()
}

// 這段代碼是當「provideDelegate」功能可用時
// 由編譯器生成的代碼:
class C {
    // 調用「provideDelegate」來創建額外的「delegate」屬性
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

請注意,provideDelegate 方法隻影響輔助屬性的創建,並不會影響爲 getter 或 setter 生成的代碼。