委托模式是軟件設(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á)式>
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 屬性
Kotlin 的標(biāo)準(zhǔn)庫中已經(jīng)內(nèi)置了很多工廠方法來實(shí)現(xiàn)屬性的委托。
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 可以用于實(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
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ù):
這個函數(shù)必須返回與屬性相同的類型(或其子類型)。
對于一個值可變(mutable)屬性(也就是說,var 屬性),除 getValue()函數(shù)之外,它的委托還必須 另外再提供一個名為setValue()的函數(shù), 這個函數(shù)接受以下參數(shù):
在每個委托屬性的實(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 相同:
在創(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 生成的代碼。
更多建議: