委托模式是軟件設(shè)計(jì)模式中的一項(xiàng)基本技巧。在委托模式中,有兩個(gè)對(duì)象參與處理同一個(gè)請(qǐng)求,接受請(qǐng)求的對(duì)象將請(qǐng)求委托給另一個(gè)對(duì)象來處理。
Kotlin 直接支持委托模式,更加優(yōu)雅,簡(jiǎn)潔。Kotlin 通過關(guān)鍵字 by 實(shí)現(xiàn)委托。
類的委托即一個(gè)類中定義的方法實(shí)際是調(diào)用另一個(gè)類的對(duì)象的方法來實(shí)現(xiàn)的。
以下示例中派生類 Derived 繼承了接口 Base 所有方法,并且委托一個(gè)傳入的 Base 類的對(duì)象來執(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 的對(duì)象示例內(nèi)部,而且編譯器將會(huì)生成繼承自 Base 接口的所有方法, 并將調(diào)用轉(zhuǎn)發(fā)給 b。
屬性委托指的是一個(gè)類的某個(gè)屬性值不是在類中直接進(jìn)行定義,而是將其托付給一個(gè)代理類,從而實(shí)現(xiàn)對(duì)該類的屬性統(tǒng)一管理。
屬性委托語法格式:
val/var <屬性名>: <類型> by <表達(dá)式>
var/val:屬性類型(可變/只讀)
屬性名:屬性名稱
類型:屬性的數(shù)據(jù)類型
表達(dá)式:委托代理類
by 關(guān)鍵字之后的表達(dá)式就是委托, 屬性的 get() 方法(以及set() 方法)將被委托給這個(gè)對(duì)象的 getValue() 和 setValue() 方法。屬性委托不必實(shí)現(xiàn)任何接口, 但必須提供 getValue() 函數(shù)(對(duì)于 var屬性,還需要 setValue() 函數(shù))。
該類需要包含 getValue() 方法和 setValue() 方法,且參數(shù) thisRef 為進(jìn)行委托的類的對(duì)象,prop 為進(jìn)行委托的屬性的對(duì)象。
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 = "Nhooo" // 調(diào)用 setValue() 函數(shù) println(e.p) }
輸出結(jié)果為:
Example@433c675d, 這里委托了 p 屬性 Example@433c675d 的 p 屬性賦值為 Nhooo Example@433c675d, 這里委托了 p 屬性
Kotlin 的標(biāo)準(zhǔn)庫中已經(jīng)內(nèi)置了很多工廠方法來實(shí)現(xiàn)屬性的委托。
lazy() 是一個(gè)函數(shù), 接受一個(gè) Lambda 表達(dá)式作為參數(shù), 返回一個(gè) Lazy <T> 示例的函數(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 可以用于實(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)里存儲(chǔ)屬性的值。 這經(jīng)常出現(xiàn)在像解析 JSON 或者做其他"動(dòng)態(tài)"事情的應(yīng)用中。 在這種情況下,你可以使用映射示例自身作為委托來實(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.soo66.com" )) // 讀取映射值 println(site.name) println(site.url) }
執(zhí)行輸出結(jié)果:
菜鳥教程 www.soo66.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.soo66.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.soo66.com -------------- Google www.google.com
notNull 適用于那些無法在初始化階段就確定屬性值的場(chǎng)合。
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ì)算。
對(duì)于只讀屬性(也就是說val屬性), 它的委托必須提供一個(gè)名為getValue()的函數(shù)。該函數(shù)接受以下參數(shù):
thisRef —— 必須與屬性所有者類型(對(duì)于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型
這個(gè)函數(shù)必須返回與屬性相同的類型(或其子類型)。
對(duì)于一個(gè)值可變(mutable)屬性(也就是說,var 屬性),除 getValue()函數(shù)之外,它的委托還必須 另外再提供一個(gè)名為setValue()的函數(shù), 這個(gè)函數(shù)接受以下參數(shù):
thisRef —— 必須與屬性所有者 類型(對(duì)于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型
new value —— 必須和屬性同類型或者是它的超類型。
在每個(gè)委托屬性的實(shí)現(xiàn)的背后,Kotlin 編譯器都會(huì)生成輔助屬性并委托給它。 例如,對(duì)于屬性 prop,生成隱藏屬性 prop$delegate,而訪問器的代碼只是簡(jiǎn)單地委托給這個(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 的示例而 this::prop 是 KProperty 類型的反射對(duì)象,該對(duì)象描述 prop 自身。
通過定義 provideDelegate 操作符,可以擴(kuò)展創(chuàng)建屬性實(shí)現(xiàn)所委托對(duì)象的邏輯。 如果 by 右側(cè)所使用的對(duì)象將 provideDelegate 定義為成員或擴(kuò)展函數(shù),那么會(huì)調(diào)用該函數(shù)來 創(chuàng)建屬性委托示例。
provideDelegate 的一個(gè)可能的使用場(chǎng)景是在創(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 —— 必須與 屬性所有者 類型(對(duì)于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型。
在創(chuàng)建 MyUI 示例期間,為每個(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 屬性。 比較對(duì)于屬性聲明 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) }
請(qǐng)注意,provideDelegate 方法只影響輔助屬性的創(chuàng)建,并不會(huì)影響為 getter 或 setter 生成的代碼。