kotlin 委託

委託模式是軟體設計模式中的一項基本技巧。在委託模式中,有兩個物件參與處理同一個請求,接受請求的物件將請求委託給另一個物件來處理。

Kotlin 直接支援委託模式,更加優雅,簡潔。Kotlin 通過關鍵字 by 實現委託。


類委託

類的委託即一個類中定義的方法實際是呼叫另一個類的物件的方法來實現的。

以下例項中派生類 Derived 繼承了介面 Base 所有方法,並且委託一個傳入的 Base 類的物件來執行這些方法。

// 建立介面
interface Base {   
    fun print()
}

// 實現此介面的被委託的類
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通過關鍵字 by 建立委託類
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() // 輸出 10
}

在 Derived 宣告中,by 子句表示,將 b 儲存在 Derived 的物件例項內部,而且編譯器將會生成繼承自 Base 介面的所有方法, 並將呼叫轉發給 b。


屬性委託

屬性委託指的是一個類的某個屬性值不是在類中直接進行定義,而是將其託付給一個代理類,從而實現對該類的屬性統一管理。

屬性委託語法格式:

val/var <屬性名>: <型別> by <表示式>
  • var/val:屬性型別(可變/只讀)
  • 屬性名:屬性名稱
  • 型別:屬性的資料型別
  • 表示式:委託代理類

by 關鍵字之後的表示式就是委託, 屬性的 get() 方法(以及set() 方法)將被委託給這個物件的 getValue() 和 setValue() 方法。屬性委託不必實現任何介面, 但必須提供 getValue() 函式(對於 var屬性,還需要 setValue() 函式)。

定義一個被委託的類

該類需要包含 getValue() 方法和 setValue() 方法,且引數 thisRef 為進行委託的類的物件,prop 為進行委託的屬性的物件。

import kotlin.reflect.KProperty
// 定義包含屬性委託的類
class Example {
    var p: String by Delegate()
}

// 委託的類
class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 這裡委託了 ${property.name} 屬性"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 屬性賦值為 $value")
    }
}
fun main(args: Array<String>) {
    val e = Example()
    println(e.p)     // 訪問該屬性,呼叫 getValue() 函式

    e.p = "itread01"   // 呼叫 setValue() 函式
    println(e.p)
}

輸出結果為:

Example@433c675d, 這裡委託了 p 屬性
Example@433c675d 的 p 屬性賦值為 itread01
Example@433c675d, 這裡委託了 p 屬性

標準委託

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

可觀察屬性 Observable

observable 可以用於實現觀察者模式。

Delegates.observable() 函式接受兩個引數: 第一個是初始化值, 第二個是屬性值變化事件的響應器(handler)。

在屬性賦值後會執行事件的響應器(handler),它有三個引數:被賦值的屬性、舊值和新值:

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("初始值") {
        prop, old, new ->
        println("舊值:$old -> 新值:$new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "第一次賦值"
    user.name = "第二次賦值"
}

執行輸出結果:

舊值:初始值 -> 新值:第一次賦值
舊值:第一次賦值 -> 新值:第二次賦值

把屬性儲存在對映中

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

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val url: String  by map
}

fun main(args: Array<String>) {
    // 建構函式接受一個對映引數
    val site = Site(mapOf(
        "name" to "入門教學",
        "url"  to "www.itread01.com"
    ))
    
    // 讀取對映值
    println(site.name)
    println(site.url)
}

執行輸出結果:

入門教學
www.itread01.com

如果使用 var 屬性,需要把 Map 換成 MutableMap:

class Site(val map: MutableMap<String, Any?>) {
    val name: String by map
    val url: String by map
}

fun main(args: Array<String>) {

    var map:MutableMap<String, Any?> = mutableMapOf(
            "name" to "入門教學",
            "url" to "www.itread01.com"
    )

    val site = Site(map)

    println(site.name)
    println(site.url)

    println("--------------")
    map.put("name", "Google")
    map.put("url", "www.google.com")

    println(site.name)
    println(site.url)

}

執行輸出結果:

入門教學
www.itread01.com
--------------
Google
www.google.com

Not Null

notNull 適用於那些無法在初始化階段就確定屬性值的場合。

class Foo {
    var notNullBar: String by Delegates.notNull<String>()
}

foo.notNullBar = "bar"
println(foo.notNullBar)

需要注意,如果屬性在賦值前就被訪問的話則會丟擲異常。


區域性委託屬性

你可以將區域性變數宣告為委託屬性。 例如,你可以使一個區域性變數惰性初始化:

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

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

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


屬性委託要求

對於只讀屬性(也就是說val屬性), 它的委託必須提供一個名為getValue()的函式。該函式接受以下引數:

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

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

對於一個值可變(mutable)屬性(也就是說,var 屬性),除 getValue()函式之外,它的委託還必須 另外再提供一個名為setValue()的函式, 這個函式接受以下引數:

  • property —— 必須是型別 KProperty<*> 或其超型別
  • new value —— 必須和屬性同類型或者是它的超型別。

翻譯規則

在每個委託屬性的實現的背後,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::prop 是 KProperty 型別的反射物件,該物件描述 prop 自身。


提供委託

通過定義 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 生成的程式碼。