委托屬性
有一些常見的屬性類型,雖然我們可以在每次需要的時(shí)候手動實(shí)現(xiàn)它們, 但是如果能夠?yàn)榇蠹野阉麄冎粚?shí)現(xiàn)一次并放入一個(gè)庫會更好。例如包括:
延遲屬性(lazy properties): 其值只在首次訪問時(shí)計(jì)算;
可觀察屬性(observable properties): 監(jiān)聽器會收到有關(guān)此屬性變更的通知;
把多個(gè)屬性儲存在一個(gè)映射(map)中,而不是每個(gè)存在單獨(dú)的字段中。
為了涵蓋這些(以及其他)情況,Kotlin 支持?委托屬性:
classExample{
varp:StringbyDelegate()
}
語法是:?val/var <屬性名>: <類型> by <表達(dá)式>。在?by?后面的表達(dá)式是該?委托, 因?yàn)閷傩詫?yīng)的?get()(與?set())會被委托給它的?getValue()?與?setValue()?方法。 屬性的委托不必實(shí)現(xiàn)任何的接口,但是需要提供一個(gè)?getValue()?函數(shù)(與?setValue()——對于?var?屬性)。 例如:
importkotlin.reflect.KProperty
?
classDelegate{
operatorfungetValue(thisRef:Any?,property:KProperty<*>):String{
return"$thisRef, thank you for delegating '${property.name}' to me!"
? ? }
operatorfunsetValue(thisRef:Any?,property:KProperty<*>,value:String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
? ? }
}
當(dāng)我們從委托到一個(gè)?Delegate?實(shí)例的?p?讀取時(shí),將調(diào)用?Delegate?中的?getValue()?函數(shù), 所以它第一個(gè)參數(shù)是讀出?p?的對象、第二個(gè)參數(shù)保存了對?p?自身的描述 (例如你可以取它的名字)。 例如:
vale=Example()
println(e.p)
輸出結(jié)果:
Example@33a17727, thank you for delegating ‘p’ to me!
類似地,當(dāng)我們給?p?賦值時(shí),將調(diào)用?setValue()?函數(shù)。前兩個(gè)參數(shù)相同,第三個(gè)參數(shù)保存將要被賦予的值:
e.p="NEW"
輸出結(jié)果:
NEW has been assigned to ‘p’ in Example@33a17727.
委托對象的要求規(guī)范可以在下文找到。
請注意,自 Kotlin 1.1 起你可以在函數(shù)或代碼塊中聲明一個(gè)委托屬性,因此它不一定是類的成員。 你可以在下文找到其示例。
標(biāo)準(zhǔn)委托
Kotlin 標(biāo)準(zhǔn)庫為幾種有用的委托提供了工廠方法。
延遲屬性 Lazy
lazy()?是接受一個(gè) lambda 并返回一個(gè)?Lazy <T>?實(shí)例的函數(shù),返回的實(shí)例可以作為實(shí)現(xiàn)延遲屬性的委托: 第一次調(diào)用?get()?會執(zhí)行已傳遞給?lazy()?的 lambda 表達(dá)式并記錄結(jié)果, 后續(xù)調(diào)用?get()?只是返回記錄的結(jié)果。
vallazyValue:Stringbylazy{
println("computed!")
"Hello"
}
?
funmain() {
println(lazyValue)
println(lazyValue)
}
Target platform:?JVMRunning on kotlin v.?1.3.70
默認(rèn)情況下,對于 lazy 屬性的求值是同步鎖的(synchronized):該值只在一個(gè)線程中計(jì)算,并且所有線程會看到相同的值。如果初始化委托的同步鎖不是必需的,這樣多個(gè)線程可以同時(shí)執(zhí)行,那么將?LazyThreadSafetyMode.PUBLICATION?作為參數(shù)傳遞給?lazy()?函數(shù)。 而如果你確定初始化將總是發(fā)生在與屬性使用位于相同的線程, 那么可以使用?LazyThreadSafetyMode.NONE?模式:它不會有任何線程安全的保證以及相關(guān)的開銷。
可觀察屬性 Observable
Delegates.observable()?接受兩個(gè)參數(shù):初始值與修改時(shí)處理程序(handler)。 每當(dāng)我們給屬性賦值時(shí)會調(diào)用該處理程序(在賦值后執(zhí)行)。它有三個(gè)參數(shù):被賦值的屬性、舊值與新值:
importkotlin.properties.Delegates
?
classUser{
varname:StringbyDelegates.observable("<no name>") {
prop,old,new->
println("$old -> $new")
? ? }
}
?
funmain() {
valuser=User()
user.name="first"
user.name="second"
}
Target platform:?JVMRunning on kotlin v.?1.3.70
如果你想截獲賦值并“否決”它們,那么使用?vetoable()?取代?observable()。 在屬性被賦新值生效之前會調(diào)用傳遞給?vetoable?的處理程序。
把屬性儲存在映射中
一個(gè)常見的用例是在一個(gè)映射(map)里存儲屬性的值。 這經(jīng)常出現(xiàn)在像解析 JSON 或者做其他“動態(tài)”事情的應(yīng)用中。 在這種情況下,你可以使用映射實(shí)例自身作為委托來實(shí)現(xiàn)委托屬性。
classUser(valmap:Map<String,Any?>) {
valname:Stringbymap
valage:Intbymap
}
在這個(gè)例子中,構(gòu)造函數(shù)接受一個(gè)映射參數(shù):
valuser=User(mapOf(
"name"to"John Doe",
"age"to25
))
委托屬性會從這個(gè)映射中取值(通過字符串鍵——屬性的名稱):
println(user.name)// Prints "John Doe"
println(user.age)// Prints 25
Target platform:?JVMRunning on kotlin v.?1.3.70
這也適用于?var?屬性,如果把只讀的?Map?換成?MutableMap?的話:
classMutableUser(valmap:MutableMap<String,Any?>) {
varname:Stringbymap
varage:Intbymap
}
局部委托屬性(自 1.1 起)
你可以將局部變量聲明為委托屬性。 例如,你可以使一個(gè)局部變量惰性初始化:
funexample(computeFoo: ()->Foo) {
valmemoizedFoobylazy(computeFoo)
?
if(someCondition&&memoizedFoo.isValid()) {
memoizedFoo.doSomething()
? ? }
}
memoizedFoo?變量只會在第一次訪問時(shí)計(jì)算。 如果?someCondition?失敗,那么該變量根本不會計(jì)算。
屬性委托要求
這里我們總結(jié)了委托對象的要求。
對于一個(gè)只讀屬性(即?val?聲明的),委托必須提供一個(gè)名為?getValue?的函數(shù),該函數(shù)接受以下參數(shù):
thisRef?—— 必須與?屬性所有者?類型(對于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或者是它的超類型;
property?—— 必須是類型?KProperty<*>?或其超類型。
這個(gè)函數(shù)必須返回與屬性相同的類型(或其子類型)。
對于一個(gè)可變屬性(即?var?聲明的),委托必須額外提供一個(gè)名為?setValue?的函數(shù),該函數(shù)接受以下參數(shù):
thisRef?—— 同?getValue();
property?—— 同?getValue();
new value —— 必須與屬性同類型或者是它的子類型。
getValue()?或/與?setValue()?函數(shù)可以通過委托類的成員函數(shù)提供或者由擴(kuò)展函數(shù)提供。 當(dāng)你需要委托屬性到原本未提供的這些函數(shù)的對象時(shí)后者會更便利。 兩函數(shù)都需要用?operator?關(guān)鍵字來進(jìn)行標(biāo)記。
委托類可以實(shí)現(xiàn)包含所需?operator?方法的?ReadOnlyProperty?或?ReadWriteProperty?接口之一。 這倆接口是在 Kotlin 標(biāo)準(zhǔn)庫中聲明的:
interfaceReadOnlyProperty<inR,outT>{
operatorfungetValue(thisRef:R,property:KProperty<*>):T
}
?
interfaceReadWriteProperty<inR,T>{
operatorfungetValue(thisRef:R,property:KProperty<*>):T
operatorfunsetValue(thisRef:R,property:KProperty<*>,value:T)
}
翻譯規(guī)則
在每個(gè)委托屬性的實(shí)現(xiàn)的背后,Kotlin 編譯器都會生成輔助屬性并委托給它。 例如,對于屬性?prop,生成隱藏屬性?prop$delegate,而訪問器的代碼只是簡單地委托給這個(gè)附加屬性:
classC{
varprop:TypebyMyDelegate()
}
?
// 這段是由編譯器生成的相應(yīng)代碼:
classC{
privatevalprop$delegate=MyDelegate()
varprop: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?自身。
請注意,直接在代碼中引用綁定的可調(diào)用引用的語法?this::prop?自 Kotlin 1.1 起才可用。
提供委托(自 1.1 起)
通過定義?provideDelegate?操作符,可以擴(kuò)展創(chuàng)建屬性實(shí)現(xiàn)所委托對象的邏輯。 如果?by?右側(cè)所使用的對象將?provideDelegate?定義為成員或擴(kuò)展函數(shù),那么會調(diào)用該函數(shù)來創(chuàng)建屬性委托實(shí)例。
provideDelegate?的一個(gè)可能的使用場景是在創(chuàng)建屬性時(shí)(而不僅在其 getter 或 setter 中)檢測屬性一致性。
例如,如果要在綁定之前檢測屬性名稱,可以這樣寫:
classResourceDelegate<T>:ReadOnlyProperty<MyUI,T>{
overridefungetValue(thisRef:MyUI,property:KProperty<*>):T{ ... }
}
classResourceLoader<T>(id:ResourceID<T>) {
operatorfunprovideDelegate(
thisRef:MyUI,
prop:KProperty<*>
):ReadOnlyProperty<MyUI,T>{
checkProperty(thisRef,prop.name)
// 創(chuàng)建委托
returnResourceDelegate()
? ? }
?
privatefuncheckProperty(thisRef:MyUI,name:String) {……}
}
?
classMyUI{
fun<T>bindResource(id:ResourceID<T>):ResourceLoader<T>{……}
?
valimagebybindResource(ResourceID.image_id)
valtextbybindResource(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”功能
classMyUI{
valimagebybindResource(ResourceID.image_id,"image")
valtextbybindResource(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?方法不存在時(shí))生成的代碼:
classC{
varprop:TypebyMyDelegate()
}
?
// 這段代碼是當(dāng)“provideDelegate”功能可用時(shí)
// 由編譯器生成的代碼:
classC{
// 調(diào)用“provideDelegate”來創(chuàng)建額外的“delegate”屬性
privatevalprop$delegate=MyDelegate().provideDelegate(this,this::prop)
varprop:Type
get()=prop$delegate.getValue(this,this::prop)
set(value:Type)=prop$delegate.setValue(this,this::prop,value)
}
請注意,provideDelegate?方法只影響輔助屬性的創(chuàng)建,并不會影響為 getter 或 setter 生成的代碼。