5.8.10.2 委托属性
原文链接: https://kotlinlang.org/docs/delegated-properties.html
5.8.10.2 委托属性
对于某些常见类型的属性,虽然你可以每次需要时都手动实现它们,但更好的做法是实现一次、放进库中,之后反复复用。例如:
- 惰性属性:值只在首次访问时计算。
- 可观察属性:该属性发生变化时会通知监听器。
- 把属性存储在映射中,而不是为每个属性单独使用一个字段。
为覆盖这些(以及其他)情况,Kotlin 支持委托属性:
1
2
3
| class Example {
var p: String by Delegate()
}
|
语法是:val/var <属性名>: <类型> by <表达式>。by 之后的表达式是一个委托,因为与该属性对应的 get()(以及 set())会被委托给它的 getValue() 和 setValue() 方法。属性委托不必实现某个接口,但必须提供 getValue() 函数(对于 var 还要提供 setValue())。
例如:
1
2
3
4
5
6
7
8
9
10
11
| 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.")
}
}
|
当你读取委托给 Delegate 实例的 p 时,会调用 Delegate 中的 getValue() 函数。它的第一个参数是你从中读取 p 的对象,第二个参数保存 p 自身的描述(例如你可以取得它的名称)。
1
2
| val e = Example()
println(e.p)
|
这会打印:
Example@33a17727, thank you for delegating 'p' to me!
类似地,当你给 p 赋值时,会调用 setValue() 函数。前两个参数相同,第三个参数保存被赋的值:
这会打印:
NEW has been assigned to 'p' in Example@33a17727.
关于委托对象的要求,规范见下文。
你可以在函数或代码块中声明委托属性,它不必是类的成员。下面有一个示例。
标准委托
Kotlin 标准库为几种有用的委托提供了工厂方法。
惰性属性
lazy() 是一个函数,它接收一个 lambda 并返回 Lazy<T> 的实例,可以作为实现惰性属性的委托。第一次调用 get() 时会执行传给 lazy() 的 lambda 并记住结果。之后对 get() 的调用只是返回记住的结果。
1
2
3
4
5
6
7
8
9
| val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
|
默认情况下,惰性属性的求值是同步的:值只在一个线程中计算,但所有线程都会看到同一个值。如果不需要对初始化委托进行同步、允许多个线程同时执行它,请把 LazyThreadSafetyMode.PUBLICATION 作为参数传给 lazy()。
如果你确定初始化总是发生在使用该属性的同一个线程中,可以使用 LazyThreadSafetyMode.NONE。它不提供任何线程安全保证,也没有相关开销。
可观察属性
Delegates.observable() 接收两个参数:初始值和一个用于处理修改的处理器。
每次给该属性赋值时都会调用这个处理器(在赋值完成之后)。它有三个参数:被赋值的属性、旧值和新值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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"
}
|
如果你想拦截赋值并否决它们,请使用 vetoable() 代替 observable()。传给 vetoable 的处理器会在给属性赋新值之前被调用。
委托给另一个属性
属性可以把它的 getter 和 setter 委托给另一个属性。这种委托对顶层属性和类属性(成员属性和扩展属性)都可用。被委托的属性可以是:
- 顶层属性
- 同一个类的成员或扩展属性
- 另一个类的成员或扩展属性
要把一个属性委托给另一个属性,请在委托名中使用 :: 限定符,例如 this::delegate 或 MyClass::delegate。
1
2
3
4
5
6
7
8
9
10
| 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 注解,并委托它的实现。
1
2
3
4
5
6
7
8
9
10
11
12
| class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// 提示:'oldName: Int' 已被弃用。
// 请改用 'newName'
myClass.oldName = 42
println(myClass.newName) // 42
}
|
把属性存储在映射中
一个常见的用例是把属性的值存储在映射中。在应用解析 JSON 或执行其他动态任务时经常遇到这种情况。此时,你可以用该映射实例本身作为委托属性的委托。
1
2
3
4
| class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
|
在这个例子中,构造函数接收一个映射:
1
2
3
4
| val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
|
委托属性通过字符串键从这个映射中取值,这些键与属性名相对应:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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) // 打印 "John Doe"
println(user.age) // 打印 25
}
|
如果改用 MutableMap 而不是只读的 Map,这种方法对 var 属性也适用:
1
2
3
4
| class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
|
局部委托属性
你可以把局部变量声明为委托属性。例如,你可以让局部变量变成惰性的:
1
2
3
4
5
6
7
| fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
|
memoizedFoo 变量只会在首次访问时计算。如果 someCondition 不成立,该变量根本不会被计算。
属性委托的要求
对于只读属性(val),委托应提供一个带以下参数的运算符函数 getValue():
thisRef 的类型必须与属性所有者相同或是其超类型(对于扩展属性,应为被扩展的类型)。property 必须是 KProperty<*> 类型或其超类型。
getValue() 必须返回与该属性相同的类型(或其子类型)。
1
2
3
4
5
6
7
8
9
10
11
| 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 必须是 KProperty<*> 类型或其超类型。value 必须与该属性类型相同(或其超类型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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 标准库中的 ReadOnlyProperty 和 ReadWriteProperty 接口,以匿名对象的形式创建委托,而无需新建类。它们提供了所需的方法:getValue() 声明在 ReadOnlyProperty 中;ReadWriteProperty 继承它并添加了 setValue()。这意味着凡是需要 ReadOnlyProperty 的地方,你都可以传入 ReadWriteProperty。
1
2
3
4
5
6
7
8
9
10
11
| 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 用作 val
var readWriteResource: Resource by resourceDelegate()
|
委托属性的转换规则
在底层,Kotlin 编译器会为某些类型的委托属性生成辅助属性,然后委托给它们。
注意: 出于优化目的,编译器在若干情况下不会生成辅助属性。可以通过委托给另一个属性的例子了解该优化。
例如,对于属性 prop,它会生成隐藏属性 prop$delegate,而访问器的代码只是委托给这个额外属性:
1
2
3
4
5
6
7
8
9
10
11
| 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 是描述 prop 自身、类型为 KProperty 的反射对象。
委托属性的优化情形
如果委托是以下情况,$delegate 字段会被省略:
1
2
3
4
| class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}
|
1
2
3
4
5
| object NamedObject {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}
val s: String by NamedObject
|
- 同一模块中带幕后字段和默认 getter 的 final
val 属性:
1
2
3
4
5
| val impl: ReadOnlyProperty<Any?, String> = ...
class A {
val s: String by impl
}
|
- 常量表达式、枚举项、
this、null。下面 this 的例子:
1
2
3
4
5
| class A {
operator fun getValue(thisRef: Any?, property: KProperty<*>) ...
val s by this
}
|
委托给另一个属性时的转换规则
委托给另一个属性时,Kotlin 编译器会生成对被引用属性的直接访问。这意味着编译器不会生成 prop$delegate 字段。这种优化有助于节省内存。
例如下面这段代码:
1
2
3
4
| class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}
|
prop 变量的属性访问器会直接调用 impl 变量,跳过委托属性的 getValue 和 setValue 运算符,因此不需要 KProperty 引用对象。
对于上面的代码,编译器会生成如下代码:
1
2
3
4
5
6
7
8
9
10
11
| class C<Type> {
private var impl: Type = ...
var prop: Type
get() = impl
set(value) {
impl = value
}
fun getProp$delegate(): Type = impl // 该方法仅用于反射
}
|
提供委托
通过定义 provideDelegate 运算符,你可以扩展创建"属性实现所委托的对象"的逻辑。如果 by 右侧使用的对象把 provideDelegate 定义为成员或扩展函数,那么在创建属性委托实例时会调用该函数。
provideDelegate 的一个可能用例是在属性初始化时检查其一致性。
例如,要在绑定时检查属性名,你可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 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)
// 创建委托
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 必须是 KProperty<*> 类型或其超类型。
在创建 MyUI 实例时,会为每个属性调用 provideDelegate 方法,并立即执行必要的校验。
如果没有这种拦截属性与其委托之间绑定的能力,要实现同样的功能就必须显式传入属性名,而这并不方便:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 不使用 "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 方法时)生成的代码比较一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| class C {
var prop: Type by MyDelegate()
}
// 当 'provideDelegate' 函数可用时,
// 编译器生成的代码:
class C {
// 调用 "provideDelegate" 来创建额外的 "delegate" 属性
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 接口,你可以不新建类就创建委托提供者。
1
2
3
4
| val provider = PropertyDelegateProvider { thisRef: Any?, property ->
ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider
|