Kotlin委託屬性
有一些常見的屬性類型,雖然我們可以在每次需要的時候手動實現它們,
但是如果能夠爲大家把他們只實現一次並放入一個庫會更好。例如包括
- 延遲屬性(lazy properties): 其值只在首次訪問時計算,
- 可觀察屬性(observable properties): 監聽器會收到有關此屬性變更的通知,
- 把多個屬性儲存在一個映射(map)中,而不是每個存在單獨的字段中。
爲了涵蓋這些(以及其他)情況,Kotlin 支持 委託屬性:
class Example {
var p: String by Delegate()
}
語法是: val/var <屬性名>: <類型> by <表達式>
。在 by{:.keyword} 後面的表達式是該 委託,
因爲屬性對應的 get()
(和 set()
)會被委託給它的 getValue()
和 setValue()
方法。
屬性的委託不必實現任何的接口,但是需要提供一個 getValue()
函數(和 setValue()
——對於 var{:.keyword} 屬性)。
例如:
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'")
}
}
當我們從委託到一個 Delegate
實例的 p
讀取時,將調用 Delegate
中的 getValue()
函數,
所以它第一個參數是讀出 p
的對象、第二個參數保存了對 p
自身的描述
(例如你可以取它的名字)。 例如:
val e = Example()
println(e.p)
輸出結果:
Example@33a17727, thank you for delegating ‘p’ to me!
類似地,當我們給 p
賦值時,將調用 setValue()
函數。前兩個參數相同,第三個參數保存將要被賦予的值:
e.p = "NEW"
輸出結果:
NEW has been assigned to ‘p’ in Example@33a17727.
委託對象的要求規範可以在下文找到。
請注意,自 Kotlin 1.1 起你可以在函數或代碼塊中聲明一個委託屬性,因此它不一定是類的成員。
你可以在下文找到其示例)。
標準委託
Kotlin 標準庫爲幾種有用的委託提供了工廠方法。
延遲屬性 Lazy
lazy()
是接受一個 lambda 並返回一個 Lazy <T>
實例的函數,返回的實例可以作爲實現延遲屬性的委託:
第一次調用 get()
會執行已傳遞給 lazy()
的 lamda 表達式並記錄結果,
後續調用 get()
只是返回記錄的結果。
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}
這個例子輸出:
computed!
Hello
Hello
默認情況下,對於 lazy 屬性的求值是**同步鎖的(synchronized)**:該值只在一個線程中計算,並且所有線程
會看到相同的值。如果初始化委託的同步鎖不是必需的,這樣多個線程
可以同時執行,那麼將 LazyThreadSafetyMode.PUBLICATION
作爲參數傳遞給 lazy()
函數。
而如果你確定初始化將總是發生在單個線程,那麼你可以使用 LazyThreadSafetyMode.NONE
模式,
它不會有任何線程安全的保證和相關的開銷。
可觀察屬性 Observable
Delegates.observable()
接受兩個參數:初始值和修改時處理程序(handler)。
每當我們給屬性賦值時會調用該處理程序(在賦值後執行)。它有三個
參數:被賦值的屬性、舊值和新值:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
這個例子輸出:
<no name> -> first
first -> second
如果你想能夠截獲一個賦值並「否決」它,就使用 vetoable()
取代 observable()
。
在屬性被賦新值生效之前會調用傳遞給 vetoable
的處理程序。
把屬性儲存在映射中
一個常見的用例是在一個映射(map)裏存儲屬性的值。
這經常出現在像解析 JSON 或者做其他「動態」事情的應用中。
在這種情況下,你可以使用映射實例自身作爲委託來實現委託屬性。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
在這個例子中,構造函數接受一個映射參數:
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
委託屬性會從這個映射中取值(通過字符串鍵——屬性的名稱):
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
這也適用於 var{:.keyword} 屬性,如果把只讀的 Map
換成 MutableMap
的話:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
局部委託屬性(自 1.1 起)
你可以將局部變量聲明爲委託屬性。
例如,你可以使一個局部變量惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo
變量只會在第一次訪問時計算。
如果 someCondition
失敗,那麼該變量根本不會計算。
屬性委託要求
這裏我們總結了委託對象的要求。
對於一個只讀屬性(即 val{:.keyword} 聲明的),委託必須提供一個名爲 getValue
的函數,該函數接受以下參數:
-
thisRef
—— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是它的超類型, -
property
—— 必須是類型KProperty<*>
或其超類型,
這個函數必須返回與屬性相同的類型(或其子類型)。
對於一個可變屬性(即 var{:.keyword} 聲明的),委託必須額外提供一個名爲 setValue
的函數,該函數接受以下參數:
-
thisRef
—— 同getValue()
, -
property
—— 同getValue()
, - new value —— 必須和屬性同類型或者是它的超類型。
getValue()
或/和 setValue()
函數可以通過委託類的成員函數提供或者由擴展函數提供。
當你需要委託屬性到原本未提供的這些函數的對象時後者會更便利。
兩函數都需要用 operator
關鍵字來進行標記。
委託類可以實現包含所需 operator
方法的 ReadOnlyProperty
或 ReadWriteProperty
接口之一。
這倆接口是在 Kotlin 標準庫中聲明的:
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
翻譯規則
在每個委託屬性的實現的背後,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 編譯器在參數中提供了關於 prop
的所有必要信息:第一個參數 this
引用到外部類 C
的實例而 this::prop
是 KProperty
類型的反射對象,該對象描述 prop
自身。
請注意,直接在代碼中引用綁定的可調用引用)的語法 this::prop
自 Kotlin 1.1 起纔可用。
提供委託(自 1.1 起)
通過定義 provideDelegate
操作符,可以擴展創建屬性實現所委託對象的邏輯。
如果 by
右側所使用的對象將 provideDelegate
定義爲成員或擴展函數,那麼會調用該函數來
創建屬性委託實例。
provideDelegate
的一個可能的使用場景是在創建屬性時(而不僅在其 getter 或 setter 中)檢查屬性一致性。
例如,如果要在綁定之前檢查屬性名稱,可以這樣寫:
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// 創建委託
}
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
的參數與 getValue
相同:
-
thisRef
—— 必須與 屬性所有者 類型(對於擴展屬性——指被擴展的類型)相同或者是它的超類型, -
property
—— 必須是類型KProperty<*>
或其超類型。
在創建 MyUI
實例期間,爲每個屬性調用 provideDelegate
方法,並立即執行必要的驗證。
如果沒有這種攔截屬性與其委託之間的綁定的能力,爲了實現相同的功能,
你必須顯式傳遞屬性名,這不是很方便:
// 檢查屬性名稱而不使用「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)
// 創建委託
}
在生成的代碼中,會調用 provideDelegate
方法來初始化輔助的 prop$delegate
屬性。
比較對於屬性聲明 val prop: Type by MyDelegate()
生成的代碼與
上面(當 provideDelegate
方法不存在時)生成的代碼:
class C {
var prop: Type by MyDelegate()
}
// 這段代碼是當「provideDelegate」功能可用時
// 由編譯器生成的代碼:
class C {
// 調用「provideDelegate」來創建額外的「delegate」屬性
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}
請注意,provideDelegate
方法隻影響輔助屬性的創建,並不會影響爲 getter 或 setter 生成的代碼。