委托模式是軟件設計模式中的一項基本技巧。在委托模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。
Kotlin 直接支持委托模式,更加優(yōu)雅,簡潔。Kotlin 通過關鍵字 by 實現(xiàn)委托。
類的委托即一個類中定義的方法實際是調用另一個類的對象的方法來實現(xiàn)的。
以下示例中派生類 Derived 繼承了接口 Base 所有方法,并且委托一個傳入的 Base 類的對象來執(zhí)行這些方法。
// 創(chuàng)建接口 interface Base { fun print() } // 實現(xiàn)此接口的被委托的類 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 接口的所有方法, 并將調用轉發(fā)給 b。
屬性委托指的是一個類的某個屬性值不是在類中直接進行定義,而是將其托付給一個代理類,從而實現(xiàn)對該類的屬性統(tǒng)一管理。
屬性委托語法格式:
val/var <屬性名>: <類型> by <表達式>
var/val:屬性類型(可變/只讀)
屬性名:屬性名稱
類型:屬性的數(shù)據(jù)類型
表達式:委托代理類
by 關鍵字之后的表達式就是委托, 屬性的 get() 方法(以及set() 方法)將被委托給這個對象的 getValue() 和 setValue() 方法。屬性委托不必實現(xiàn)任何接口, 但必須提供 getValue() 函數(shù)(對于 var屬性,還需要 setValue() 函數(shù))。
該類需要包含 getValue() 方法和 setValue() 方法,且參數(shù) 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() 函數(shù) e.p = "Nhooo" // 調用 setValue() 函數(shù) println(e.p) }
輸出結果為:
Example@433c675d, 這里委托了 p 屬性 Example@433c675d 的 p 屬性賦值為 Nhooo Example@433c675d, 這里委托了 p 屬性
Kotlin 的標準庫中已經內置了很多工廠方法來實現(xiàn)屬性的委托。
lazy() 是一個函數(shù), 接受一個 Lambda 表達式作為參數(shù), 返回一個 Lazy <T> 示例的函數(shù),返回的示例可以作為實現(xiàn)延遲屬性的委托: 第一次調用 get() 會執(zhí)行已傳遞給 lazy() 的 lamda 表達式并記錄結果, 后續(xù)調用 get() 只是返回記錄的結果。
val lazyValue: String by lazy { println("computed!") // 第一次調用輸出,第二次調用不執(zhí)行 "Hello" } fun main(args: Array<String>) { println(lazyValue) // 第一次執(zhí)行,執(zhí)行兩次輸出表達式 println(lazyValue) // 第二次執(zhí)行,只輸出返回值 }
執(zhí)行輸出結果:
computed! Hello Hello
observable 可以用于實現(xiàn)觀察者模式。
Delegates.observable() 函數(shù)接受兩個參數(shù): 第一個是初始化值, 第二個是屬性值變化事件的響應器(handler)。
在屬性賦值后會執(zhí)行事件的響應器(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í)行輸出結果:
舊值:初始值 -> 新值:第一次賦值 舊值:第一次賦值 -> 新值:第二次賦值
一個常見的用例是在一個映射(map)里存儲屬性的值。 這經常出現(xiàn)在像解析 JSON 或者做其他"動態(tài)"事情的應用中。 在這種情況下,你可以使用映射示例自身作為委托來實現(xiàn)委托屬性。
class Site(val map: Map<String, Any?>) { val name: String by map val url: String by map } fun main(args: Array<String>) { // 構造函數(shù)接受一個映射參數(shù) val site = Site(mapOf( "name" to "菜鳥教程", "url" to "www.jixiangtaizi.com.cn" )) // 讀取映射值 println(site.name) println(site.url) }
執(zhí)行輸出結果:
菜鳥教程 www.jixiangtaizi.com.cn
如果使用 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.jixiangtaizi.com.cn" ) 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í)行輸出結果:
菜鳥教程 www.jixiangtaizi.com.cn -------------- Google www.google.com
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 —— 必須與屬性所有者類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型
這個函數(shù)必須返回與屬性相同的類型(或其子類型)。
對于一個值可變(mutable)屬性(也就是說,var 屬性),除 getValue()函數(shù)之外,它的委托還必須 另外再提供一個名為setValue()的函數(shù), 這個函數(shù)接受以下參數(shù):
thisRef —— 必須與屬性所有者 類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型
new value —— 必須和屬性同類型或者是它的超類型。
在每個委托屬性的實現(xiàn)的背后,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 編譯器在參數(shù)中提供了關于 prop 的所有必要信息:第一個參數(shù) this 引用到外部類 C 的示例而 this::prop 是 KProperty 類型的反射對象,該對象描述 prop 自身。
通過定義 provideDelegate 操作符,可以擴展創(chuàng)建屬性實現(xiàn)所委托對象的邏輯。 如果 by 右側所使用的對象將 provideDelegate 定義為成員或擴展函數(shù),那么會調用該函數(shù)來 創(chuàng)建屬性委托示例。
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 —— 必須與 屬性所有者 類型(對于擴展屬性——指被擴展的類型)相同或者是它的超類型
property —— 必須是類型 KProperty 或其超類型。
在創(chuàng)建 MyUI 示例期間,為每個屬性調用 provideDelegate 方法,并立即執(zhí)行必要的驗證。
如果沒有這種攔截屬性與其委托之間的綁定的能力,為了實現(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)建委托 }
在生成的代碼中,會調用 provideDelegate 方法來初始化輔助的 prop$delegate 屬性。 比較對于屬性聲明 val prop: Type by MyDelegate() 生成的代碼與 上面(當 provideDelegate 方法不存在時)生成的代碼:
class C { var prop: Type by MyDelegate() } // 這段代碼是當“provideDelegate”功能可用時 // 由編譯器生成的代碼: class C { // 調用“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 生成的代碼。