【Android進(jìn)階】kotlin 委托

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

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

類委托

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

以下實(shí)例中派生類 Derived 繼承了接口 Base 所有方法嗅蔬,并且委托一個(gè)傳入的 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)部,而且編譯器將會(huì)生成繼承自 Base 接口的所有方法, 并將調(diào)用轉(zhuǎn)發(fā)給 b猬腰。

屬性委托

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

屬性委托語法格式:

val/var <屬性名>: <類型> by <表達(dá)式>
  • var/val:屬性類型(可變/只讀)
  • 屬性名:屬性名稱
  • 類型:屬性的數(shù)據(jù)類型
  • 表達(dá)式:委托代理類
    by 關(guān)鍵字之后的表達(dá)式就是委托, 屬性的 get() 方法(以及set() 方法)將被委托給這個(gè)對象的 getValue() 和 setValue() 方法盒延。屬性委托不必實(shí)現(xiàn)任何接口, 但必須提供 getValue() 函數(shù)(對于 var屬性,還需要 setValue() 函數(shù))。

定義一個(gè)被委托的類

該類需要包含 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 = "Runoob"   // 調(diào)用 setValue() 函數(shù)
    println(e.p)
}

輸出結(jié)果為:

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

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

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

延遲屬性 Lazy

lazy() 是一個(gè)函數(shù), 接受一個(gè) Lambda 表達(dá)式作為參數(shù), 返回一個(gè) Lazy <T> 實(shí)例的函數(shù),返回的實(shí)例可以作為實(shí)現(xiàn)延遲屬性的委托: 第一次調(diào)用 get() 會(huì)執(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ù)接受兩個(gè)參數(shù): 第一個(gè)是初始化值, 第二個(gè)是屬性值變化事件的響應(yīng)器(handler)繁堡。

在屬性賦值后會(huì)執(zhí)行事件的響應(yīng)器(handler)沈善,它有三個(gè)參數(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é)果:

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

把屬性儲存在映射中

一個(gè)常見的用例是在一個(gè)映射(map)里存儲屬性的值椭蹄。 這經(jīng)常出現(xiàn)在像解析 JSON 或者做其他"動(dòng)態(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ù)接受一個(gè)映射參數(shù)
    val site = Site(mapOf(
        "name" to "菜鳥教程",
        "url"  to "www.runoob.com"
    ))
    
    // 讀取映射值
    println(site.name)
    println(site.url)
}

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

菜鳥教程
www.runoob.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.runoob.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.runoob.com
--------------
Google
www.google.com

Not Null

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

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

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

需要注意金度,如果屬性在賦值前就被訪問的話則會(huì)拋出異常猜极。

局部委托屬性

你可以將局部變量聲明為委托屬性跟伏。 例如翩瓜,你可以使一個(gè)局部變量惰性初始化:

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

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

memoizedFoo 變量只會(huì)在第一次訪問時(shí)計(jì)算。 如果 someCondition 失敗辞色,那么該變量根本不會(huì)計(jì)算。

屬性委托要求

對于只讀屬性(也就是說val屬性), 它的委托必須提供一個(gè)名為getValue()的函數(shù)层亿。該函數(shù)接受以下參數(shù):

  • thisRef —— 必須與屬性所有者類型(對于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或者是它的超類型
  • property —— 必須是類型 KProperty<*> 或其超類型
    這個(gè)函數(shù)必須返回與屬性相同的類型(或其子類型)匿又。

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

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

翻譯規(guī)則

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

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 的所有必要信息:第一個(gè)參數(shù) this 引用到外部類 C 的實(shí)例而 this::prop 是 KProperty 類型的反射對象房午,該對象描述 prop 自身丹允。

提供委托

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

provideDelegate 的一個(gè)可能的使用場景是在創(chuàng)建屬性時(shí)(而不僅在其 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í)例期間恳谎,為每個(gè)屬性調(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)建委托
}

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

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

// 這段代碼是當(dāng)“provideDelegate”功能可用時(shí)
// 由編譯器生成的代碼:
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)建剥汤,并不會(huì)影響為 getter 或 setter 生成的代碼吭敢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末暮芭,一起剝皮案震驚了整個(gè)濱河市辕宏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌凄鼻,老刑警劉巖聚假,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膘格,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)游岳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喷户,“玉大人访锻,你說我怎么就攤上這事期犬。” “怎么了龟虎?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拱雏。 經(jīng)常有香客問我,道長铸抑,這世上最難降的妖魔是什么鹊汛? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任柒昏,我火速辦了婚禮熙揍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘届囚。我一直安慰自己意系,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布痰催。 她就那樣靜靜地躺著迎瞧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缝裁。 梳的紋絲不亂的頭發(fā)上捷绑,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天氢妈,我揣著相機(jī)與錄音,去河邊找鬼厕怜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛琅捏,可吹牛的內(nèi)容都是我干的递雀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼搜吧,長吁一口氣:“原來是場噩夢啊……” “哼滤奈!你這毒婦竟也來了蜒程?” 一聲冷哼從身側(cè)響起伺帘,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎领炫,沒想到半個(gè)月后帝洪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡葱峡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泌参。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片常空。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖铣缠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝗蛙,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布哮内,位于F島的核電站北发,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏琳拨。R本人自食惡果不足惜狱庇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一僵井、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧批什,春花似錦社搅、人聲如沸形葬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猖腕。三九已至倘感,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淤年,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工溉苛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豹休,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓凤巨,卻偏偏與公主長得像敢茁,于是被迫代替她去往敵國和親留美。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容