Kotlin Delegation
Delegation PatternDelegation pattern is a fundamental technique in software design patterns. In the delegation pattern, two objects participate in handling the same request. The object that receives the request delegates the request to another object for processing.
Kotlin directly supports the delegation pattern, making it more elegant and concise. Kotlin implements delegation through the keyword `by`.
Class Delegation
Class delegation means that the methods defined in a class are actually implemented by calling the methods of an object of another class.
In the following example, the derived class `Derived` inherits all methods of the interface `Base` and delegates to an incoming object of the `Base` class to execute these methods.
// 创建接口
interface Base {
fun print()
}
// 实现此接口的被委托的类
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
}
In the `Derived` declaration, the `by` clause means that `b` is stored inside the object instance of `Derived`, and the compiler will generate all methods inherited from the `Base` interface and forward the calls to `b`.
Property Delegation
Property delegation means that a property value of a class is not defined directly in the class, but is entrusted to a delegate class, thereby achieving unified management of the class's properties.
Property delegation syntax:
val/var <属性名>: <类型> by <表达式>
- var/val: property type (mutable/read-only)
- Property name: the name of the property
- Type: the data type of the property
- Expression: the delegate class
The expression after the `by` keyword is the delegate. The property's `get()` method (and `set()` method) will be delegated to the `getValue()` and `setValue()` methods of this object. Property delegation does not need to implement any interface, but it must provide the `getValue()` function (for `var` properties, a `setValue()` function is also required).
Defining a Delegated Class
This class needs to contain the `getValue()` and `setValue()` methods. The parameter `thisRef` is the object of the class performing the delegation, and `prop` is the object of the property being delegated.
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() 函数
e.p = "Example" // 调用 setValue() 函数
println(e.p)
}
The output result is:
Example@433c675d, 这里委托了 p 属性 Example@433c675d 的 p 属性赋值为 Example Example@433c675d, 这里委托了 p 属性
Standard Delegates
Kotlin's standard library has many built-in factory methods to implement property delegation.
Lazy Properties
`lazy()` is a function that accepts a Lambda expression as a parameter and returns an instance of `Lazy<T>`. The returned instance can be used as a delegate to implement lazy properties: the first call to `get()` executes the lambda expression passed to `lazy()` and records the result; subsequent calls to `get()` simply return the recorded result.
val lazyValue: String by lazy {
println("computed!") // 第一次调用输出,第二次调用不执行
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue) // 第一次执行,执行两次输出表达式
println(lazyValue) // 第二次执行,只输出返回值
}
The output after execution is:
computed! Hello Hello
Observable Properties
`observable` can be used to implement the observer pattern.
The `Delegates.observable()` function accepts two parameters: the first is the initial value, and the second is the handler for the property value change event.
After the property is assigned, the event handler is executed. It has three parameters: the property being assigned, the old value, and the new value:
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 = "第二次赋值"
}
The output after execution is:
Old value: initial value -> New value: first assignment Old value: first assignment -> New value: second assignment
Storing Properties in a Map
A common use case is storing property values in a map. This often appears in applications that parse JSON or do other "dynamic" things. In this case, you can use the map instance itself as a delegate to implement delegated properties.
class Site(val map: Map<String, Any?>) {
val name: String by map
val url: String by map
}
fun main(args: Array<String>) {
// 构造函数接受一个映射参数
val site = Site(mapOf(
"name" to "Example",
"url" to "www.example.com"
))
// 读取映射值
println(site.name)
println(site.url)
}
The output after execution is:
Example www.example.com
If you use a `var` property, you need to replace `Map` with `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 "Example",
"url" to "www.example.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)
}
The output after execution is:
Example www.example.com -------------- Google www.google.com
Not Null
`notNull` is suitable for situations where the property value cannot be determined during the initialization phase.
class Foo {
var notNullBar: String by Delegates.notNull<String>()
}
foo.notNullBar = "bar"
println(foo.notNullBar)
It should be noted that if the property is accessed before assignment, an exception will be thrown.
Local Delegated Properties
You can declare local variables as delegated properties. For example, you can make a local variable lazily initialized:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
The `memoizedFoo` variable is only computed on first access. If `someCondition` fails, the variable will not be computed at all.
Property Delegation Requirements
For read-only properties (i.e., `val` properties), their delegate must provide a function named `getValue()`. This function accepts the following parameters:
- `thisRef` — must be the same type as the property owner type (for extension properties, the extended type) or a supertype of it.
- `property` — must be of type `KProperty<*>` or a supertype of it.
This function must return the same type as the property (or a subtype of it).
For a mutable property (i.e., a `var` property), in addition to the `getValue()` function, its delegate must also provide a function named `setValue()`. This function accepts the following parameters:
- `property` — must be of type `KProperty<*>` or a supertype of it.
- `new value` — must be the same type as the property or a supertype of it.
Translation Rules
Behind every delegated property implementation, the Kotlin compiler generates an auxiliary property and delegates to it. For example, for property `prop`, a hidden property `prop$delegate` is generated, and the accessor code simply delegates to this additional property:
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)
}
The Kotlin compiler provides all necessary information about `prop` in the parameters: the first parameter `this` refers to an instance of the outer class `C`, and `this::prop` is a reflection object of type `KProperty`, which describes `prop` itself.
Providing a Delegate
By defining the `provideDelegate` operator, you can extend the logic for creating the object to which the property implementation is delegated. If the object used on the right side of `by` defines `provideDelegate` as a member or extension function, that function will be called to create the property delegate instance.
One possible use case for `provideDelegate` is to check property consistency when the property is created (not just in its getter or setter).
For example, if you want to check the property name before binding, you can write:
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)
}
The parameters of `provideDelegate` are the same as those of `getValue`:
- `thisRef` — must be the same type as the property owner type (for extension properties, the extended type) or a supertype of it.
- `property` — must be of type `KProperty<*>` or a supertype of it.
During the creation of a `MyUI` instance, the `provideDelegate` method is called for each property, and the necessary validation is executed immediately.
Without this ability to intercept the binding between the property and its delegate, to achieve the same functionality you would have to explicitly pass the property name, which is not very convenient:
// 检查属性名称而不使用“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)
// 创建委托
}
In the generated code, the `provideDelegate` method is called to initialize the auxiliary `prop$delegate` property. Compare the code generated for the property declaration `val prop: Type by MyDelegate()` with the code generated above (when the `provideDelegate` method does not exist):
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)
}
Note that the `provideDelegate` method only affects the creation of the auxiliary property and does not affect the code generated for the getter or setter.
Other Extensions