kotlin 委托

2022-12-03 15:24 更新

委托模式是軟件設(shè)計模式中的一項(xiàng)基本技巧。在委托模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。

Kotlin 直接支持委托模式,更加優(yōu)雅,簡潔。Kotlin 通過關(guān)鍵字 by 實(shí)現(xiàn)委托。

類委托

類的委托即一個類中定義的方法實(shí)際是調(diào)用另一個類的對象的方法來實(shí)現(xiàn)的。

以下實(shí)例中派生類 Derived 繼承了接口 Base 所有方法,并且委托一個傳入的 Base 類的對象來執(zhí)行這些方法。

// 創(chuàng)建接口
interface Base {   
    fun print()
}

// 實(shí)現(xiàn)此接口的被委托的類
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

// 通過關(guān)鍵字 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 的對象實(shí)例內(nèi)部,而且編譯器將會生成繼承自 Base 接口的所有方法, 并將調(diào)用轉(zhuǎn)發(fā)給 b。

屬性委托

屬性委托指的是一個類的某個屬性值不是在類中直接進(jìn)行定義,而是將其托付給一個代理類,從而實(shí)現(xiàn)對該類的屬性統(tǒng)一管理。

屬性委托語法格式:

val/var <屬性名>: <類型> by <表達(dá)式>
  • var/val:屬性類型(可變/只讀)
  • 屬性名:屬性名稱
  • 類型:屬性的數(shù)據(jù)類型
  • 表達(dá)式:委托代理類

by 關(guān)鍵字之后的表達(dá)式就是委托, 屬性的 get() 方法(以及set() 方法)將被委托給這個對象的 getValue() 和 setValue() 方法。屬性委托不必實(shí)現(xiàn)任何接口, 但必須提供 getValue() 函數(shù)(對于 var屬性,還需要 setValue() 函數(shù))。

定義一個被委托的類

該類需要包含 getValue() 方法和 setValue() 方法,且參數(shù) thisRef 為進(jìn)行委托的類的對象,prop 為進(jìn)行委托的屬性的對象。

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)     // 訪問該屬性,調(diào)用 getValue() 函數(shù)

    e.p = "W3cschool"   // 調(diào)用 setValue() 函數(shù)
    println(e.p)
}

輸出結(jié)果為:

Example@433c675d, 這里委托了 p 屬性
Example@433c675d 的 p 屬性賦值為 W3cschool
Example@433c675d, 這里委托了 p 屬性

標(biāo)準(zhǔn)委托

Kotlin 的標(biāo)準(zhǔn)庫中已經(jīng)內(nèi)置了很多工廠方法來實(shí)現(xiàn)屬性的委托。

延遲屬性 Lazy

lazy() 是一個函數(shù), 接受一個 Lambda 表達(dá)式作為參數(shù), 返回一個 Lazy <T> 實(shí)例的函數(shù),返回的實(shí)例可以作為實(shí)現(xiàn)延遲屬性的委托: 第一次調(diào)用 get() 會執(zhí)行已傳遞給 lazy() 的 lamda 表達(dá)式并記錄結(jié)果, 后續(xù)調(diào)用 get() 只是返回記錄的結(jié)果。

val lazyValue: String by lazy {
    println("computed!")     // 第一次調(diào)用輸出,第二次調(diào)用不執(zhí)行
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)   // 第一次執(zhí)行,執(zhí)行兩次輸出表達(dá)式
    println(lazyValue)   // 第二次執(zhí)行,只輸出返回值
}

執(zhí)行輸出結(jié)果:

computed!
Hello
Hello

可觀察屬性 Observable

observable 可以用于實(shí)現(xiàn)觀察者模式。

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

在屬性賦值后會執(zhí)行事件的響應(yīng)器(handler),它有三個參數(shù):被賦值的屬性、舊值和新值:

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 = "第二次賦值"
}

執(zhí)行輸出結(jié)果:

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

把屬性儲存在映射中

一個常見的用例是在一個映射(map)里存儲屬性的值。 這經(jīng)常出現(xiàn)在像解析 JSON 或者做其他"動態(tài)"事情的應(yīng)用中。 在這種情況下,你可以使用映射實(shí)例自身作為委托來實(shí)現(xiàn)委托屬性。

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

fun main(args: Array<String>) {
    // 構(gòu)造函數(shù)接受一個映射參數(shù)
    val site = Site(mapOf(
        "name" to "編程獅",
        "url"  to "www.w3cschool.com"
)) // 讀取映射值 println(site.name) println(site.url) }

執(zhí)行輸出結(jié)果:

編程獅
www.w3cschool.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.w3cschool.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) }

執(zhí)行輸出結(jié)果:

編程獅
www.w3cschool.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()的函數(shù)。該函數(shù)接受以下參數(shù):

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

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

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

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

翻譯規(guī)則

在每個委托屬性的實(shí)現(xiàn)的背后,Kotlin 編譯器都會生成輔助屬性并委托給它。 例如,對于屬性 prop,生成隱藏屬性 prop$delegate,而訪問器的代碼只是簡單地委托給這個附加屬性:

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

// 這段是由編譯器生成的相應(yīng)代碼:
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 編譯器在參數(shù)中提供了關(guān)于 prop 的所有必要信息:第一個參數(shù) this 引用到外部類 C 的實(shí)例而 this::prop 是 KProperty 類型的反射對象,該對象描述 prop 自身。

提供委托

通過定義 provideDelegate 操作符,可以擴(kuò)展創(chuàng)建屬性實(shí)現(xiàn)所委托對象的邏輯。 如果 by 右側(cè)所使用的對象將 provideDelegate 定義為成員或擴(kuò)展函數(shù),那么會調(diào)用該函數(shù)來 創(chuàng)建屬性委托實(shí)例。

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

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

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

    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 的參數(shù)與 getValue 相同:

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

在創(chuàng)建 MyUI 實(shí)例期間,為每個屬性調(diào)用 provideDelegate 方法,并立即執(zhí)行必要的驗(yàn)證。

如果沒有這種攔截屬性與其委托之間的綁定的能力,為了實(shí)現(xiàn)相同的功能, 你必須顯式傳遞屬性名,這不是很方便:

// 檢查屬性名稱而不使用“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)
   // 創(chuàng)建委托
}

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

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

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

請注意,provideDelegate 方法只影響輔助屬性的創(chuàng)建,并不會影響為 getter 或 setter 生成的代碼。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號