跳至主要内容

委託屬性 (Delegated properties)

有些常見種類的屬性,即使每次需要時都可以手動實作,但最好還是實作一次,將它們新增到函式庫中,以便日後重複使用。 例如:

  • 惰性 (Lazy) 屬性:值僅在第一次存取時計算。
  • 可觀察 (Observable) 屬性:監聽器會收到此屬性變更的通知。
  • 將屬性儲存在 map 中,而不是為每個屬性使用單獨的欄位。

為了涵蓋這些(和其他)情況,Kotlin 支援 委託屬性 (delegated properties)

class Example {
var p: String by Delegate()
}

語法是:val/var <屬性名稱 (property name)>: <類型 (Type)> by <表達式 (expression)>by 後面的表達式是 委託 (delegate),因為對應於該屬性的 get()(和 set())將委託給其 getValue()setValue() 方法。 屬性委託不必實作介面,但它們必須提供 getValue() 函式(以及 varsetValue())。

例如:

import kotlin.reflect.KProperty

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.")
}
}

當你從 p 讀取時,它會委託給 Delegate 的一個實例,並呼叫 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.

委託物件的需求規範可以在下方找到。

你可以在函式或程式碼區塊內宣告委託屬性;它不必是類別的成員。 你可以在下方找到範例。

標準委託 (Standard delegates)

Kotlin 標準函式庫為幾種有用的委託提供了工廠方法 (factory methods)。

惰性屬性 (Lazy properties)

lazy() 是一個接受 lambda 表達式並回傳 Lazy<T> 實例的函式,可用作實作惰性屬性的委託。 第一次呼叫 get() 會執行傳遞給 lazy() 的 lambda 表達式並記住結果。 後續呼叫 get() 只會回傳記住的結果。

val lazyValue: String by lazy {
println("computed!")
"Hello"
}

fun main() {
println(lazyValue)
println(lazyValue)
}

預設情況下,惰性屬性的評估是 同步的 (synchronized):該值僅在一個執行緒中計算,但所有執行緒 都會看到相同的值。 如果不需要初始化委託的同步來允許多個執行緒 同時執行它,則將 LazyThreadSafetyMode.PUBLICATION 作為參數傳遞給 lazy()

如果你確定初始化始終發生在使用該屬性的同一個執行緒中, 則可以使用 LazyThreadSafetyMode.NONE。 它不承擔任何執行緒安全保證和相關的額外負擔。

可觀察屬性 (Observable properties)

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() {
val user = User()
user.name = "first"
user.name = "second"
}

如果你想要攔截賦值並 否決 (veto) 它們,請使用 vetoable() 而不是 observable()。 傳遞給 vetoable 的處理常式將在賦予新的屬性值 之前 呼叫。

委託給另一個屬性 (Delegating to another property)

一個屬性可以將其 getter 和 setter 委託給另一個屬性。 這種委託適用於 頂層和類別屬性(成員和擴充)。 委託屬性可以是:

  • 一個頂層屬性
  • 同一個類別的成員或擴充屬性
  • 另一個類別的成員或擴充屬性

要將屬性委託給另一個屬性,請在委託名稱中使用 :: 限定詞,例如,this::delegateMyClass::delegate

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt

val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

例如,當你想要以向後相容的方式重新命名屬性時,這可能會很有用:引入一個新屬性, 使用 @Deprecated 註解標記舊屬性,並委託其實作。

class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// Notification: 'oldName: Int' is deprecated.
// Use 'newName' instead
myClass.oldName = 42
println(myClass.newName) // 42
}

將屬性儲存在 map 中 (Storing properties in a map)

一個常見的用例是將屬性的值儲存在 map 中。 這在諸如解析 JSON 或執行其他動態任務之類的應用程式中經常出現。 在這種情況下,你可以使用 map 實例本身作為委託屬性的委託。

class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

在此範例中,建構函式 (constructor) 接受一個 map:

val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

委託屬性透過字串鍵從這個 map 中取得值,這些字串鍵與屬性的名稱相關聯:

class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

println(user.name) // Prints "John Doe"
println(user.age) // Prints 25

}

如果使用 MutableMap 而不是唯讀 Map,這也適用於 var 的屬性:

class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}

區域委託屬性 (Local delegated properties)

你可以將區域變數宣告為委託屬性。 例如,你可以使區域變數為惰性的:

fun example(computeFoo: () `->` Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}

memoizedFoo 變數僅在第一次存取時計算。 如果 someCondition 失敗,則根本不會計算該變數。

屬性委託需求 (Property delegate requirements)

對於 唯讀 屬性 (val),委託應提供一個具有以下參數的運算子函式 getValue()

  • thisRef 必須與 屬性擁有者 (property owner) 的類型相同,或是其父類型(對於擴充屬性,它應該是要擴充的類型)。
  • property 必須是 KProperty<*> 類型或其父類型。

getValue() 必須回傳與屬性相同的類型(或其子類型)。

class Resource

class Owner {
val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}

對於 可變 屬性 (var),委託還必須提供一個具有以下參數的運算子函式 setValue()

  • thisRef 必須與 屬性擁有者 (property owner) 的類型相同,或是其父類型(對於擴充屬性,它應該是要擴充的類型)。
  • property 必須是 KProperty<*> 類型或其父類型。
  • value 必須與屬性具有相同的類型(或其父類型)。
class Resource

class Owner {
var varResource: Resource by ResourceDelegate()
}

class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}

getValue() 和/或 setValue() 函式可以作為委託類別的成員函式或作為擴充函式提供。 當你需要將屬性委託給最初不提供這些函式的物件時,後者會很方便。 這兩個函式都需要使用 operator 關鍵字進行標記。

你可以建立委託作為匿名物件,而無需建立新類別,方法是使用 Kotlin 標準函式庫中的介面 ReadOnlyPropertyReadWriteProperty。 它們提供了所需的方法:getValue()ReadOnlyProperty 中宣告;ReadWriteProperty 擴充它並新增 setValue()。 這表示你可以在預期 ReadOnlyProperty 的任何地方傳遞 ReadWriteProperty

fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
object : ReadWriteProperty<Any?, Resource> {
var curValue = resource
override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
curValue = value
}
}

val readOnlyResource: Resource by resourceDelegate() // ReadWriteProperty as val
var readWriteResource: Resource by resourceDelegate()

委託屬性的翻譯規則 (Translation rules for delegated properties)

在底層,Kotlin 編譯器會為某些種類的委託屬性產生輔助屬性,然後委託給它們。

備註

為了最佳化目的,在某些情況下,編譯器 不會 產生輔助屬性。 請透過委託給另一個屬性的範例 瞭解有關最佳化的資訊。

例如,對於屬性 prop,它會產生隱藏屬性 prop$delegate,並且存取子 (accessors) 的程式碼 只會委託給這個額外的屬性:

class C {
var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
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 本身。

委託屬性的最佳化案例 (Optimized cases for delegated properties)

如果委託是以下情況,則會省略 $delegate 欄位:

  • 參考的屬性:

    class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
    }
  • 具名物件 (named object):

    object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
    }

    val s: String by NamedObject
  • 在同一個模組中具有 backing field 和預設 getter 的 final val 屬性:

    val impl: ReadOnlyProperty<Any?, String> = ...

    class A {
    val s: String by impl
    }
  • 常數表達式、enum 條目、thisnullthis 的範例:

    class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
    }

委託給另一個屬性時的翻譯規則 (Translation rules when delegating to another property)

委託給另一個屬性時,Kotlin 編譯器會產生對被參考屬性的直接存取。 這表示編譯器不會產生欄位 prop$delegate。 這種最佳化有助於節省記憶體。

以以下程式碼為例:

class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}

prop 變數的屬性存取子直接呼叫 impl 變數,跳過委託屬性的 getValuesetValue 運算符, 因此不需要 KProperty 參考物件。

對於上面的程式碼,編譯器會產生以下程式碼:

class C<Type> {
private var impl: Type = ...

var prop: Type
get() = impl
set(value) {
impl = value
}

fun getProp$delegate(): Type = impl // This method is needed only for reflection
}

提供委託 (Providing a delegate)

透過定義 provideDelegate 運算符,你可以擴充用於建立委託屬性實作的物件的邏輯。 如果 by 右側使用的物件將 provideDelegate 定義為成員或擴充函式,則將呼叫該函式以建立屬性委託實例。

provideDelegate 的一種可能的用例是在初始化時檢查屬性的一致性。

例如,要在綁定之前檢查屬性名稱,你可以編寫如下內容:

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}

class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// create delegate
return ResourceDelegate()
}

private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}

provideDelegate 的參數與 getValue 的參數相同:

  • thisRef 必須與 屬性擁有者 (property owner) 的類型相同,或是其父類型(對於擴充屬性,它應該是要擴充的類型);
  • property 必須是 KProperty<*> 類型或其父類型。

在建立 MyUI 實例期間,會為每個屬性呼叫 provideDelegate 方法,並且它會立即執行 必要的驗證。

如果沒有這種攔截屬性和其委託之間的綁定的能力,為了實現相同的功能, 你必須顯式傳遞屬性名稱,這不是很方便:

// Checking the property name without "provideDelegate" functionality
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)
// create delegate
}

在產生的程式碼中,呼叫 provideDelegate 方法以初始化輔助 prop$delegate 屬性。 將屬性宣告 val prop: Type by MyDelegate() 產生的程式碼與 上面(當 provideDelegate 方法不存在時)產生的程式碼進行比較:

class C {
var prop: Type by MyDelegate()
}

// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
// calling "provideDelegate" to create the additional "delegate" property
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

請注意,provideDelegate 方法僅影響輔助屬性的建立,而不影響為 getter 或 setter 產生的程式碼。

使用標準函式庫中的 PropertyDelegateProvider 介面,你可以建立委託提供者,而無需建立新類別。

val provider = PropertyDelegateProvider { thisRef: Any?, property `->`
ReadOnlyProperty<Any?, Int> {_, property `->` 42 }
}
val delegate: Int by provider