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 = "zaixian"   // 調用 setValue() 函數
    println(e.p)
}

輸出結果為:

Example@433c675d, 這裏委託了 p 屬性

Example@433c675d 的 p 屬性賦值為 zaixian
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 "IT研修",
        "url"  to "www.xuhuhu.com"
    ))

    // 讀取映射值

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

執行輸出結果:

IT研修
www.xuhuhu.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 "IT研修",
            "url" to "www.xuhuhu.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)

}

執行輸出結果:

IT研修
www.xuhuhu.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 生成的代碼。