Kotlin 语言参考文档 中文版 Help

委托属性

有许多非常具有共性的属性, 虽然你可以在每个需要这些属性的类中手工地实现它们, 但是, 如果能够只实现一次, 然后将它放在库中, 供所有需要者重复使用, 那将会很有帮助. 例如:

  • 延迟加载(lazy) 属性: 属性值只在初次访问时才会计算.

  • 可观察(observable) 属性: 属性发生变化时, 监听器会收到通知.

  • 将多个属性保存在一个 map 内, 而不是将每个属性保存在一个独立的域内.

为了解决这些问题(以及其它问题), Kotlin 允许 委托属性(delegated property):

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

委托属性的语法是: val/var <property name>: <Type> by <expression>. 其中 by 关键字之后的表达式就是 委托, 属性的 get() 方法(以及 set() 方法) 将被委托给这个对象的 getValue()setValue() 方法. 属性委托不必实现接口, 但必须提供 getValue() 函数(对于 var 属性, 还需要 setValue() 函数).

示例:

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 的实例, 那么当你读取属性值时, 就会调用到 DelegategetValue() 函数. 此时函数收到的第一个参数将是你访问的属性 p 所属的对象实例, 第二个参数将是 p 属性本身的描述信息(比如, 你可以从这里得到属性名称).

val e = Example() println(e.p)

这段代码的打印结果将是:

Example@33a17727, thank you for delegating 'p' to me!

类似的, 当你向属性 p 赋值时, 将会调用到 setValue() 函数. 这个函数收到的前两个参数与 getValue() 函数相同, 第三个参数将是即将赋给属性的新值:

e.p = "NEW"

这段代码的打印结果将是:

NEW has been assigned to 'p' in Example@33a17727.

对属性委托对象的要求, 详细的说明请参见下文.

你可以在函数内, 或者一个代码段内定义委托属性, 委托属性不一定需要是类的成员. 参见 示例.

标准委托

Kotlin 标准库中提供了一些工厂方法, 可以实现几种很有用的委托.

延迟加载(Lazy)属性

lazy() 是一个函数, 接受一个 Lambda 表达式作为参数, 返回一个 Lazy<T> 类型的实例, 这个实例可以作为一个委托, 实现延迟加载(lazy)属性. 第一次调用 get() 时, 将会执行 lazy() 函数受到的 Lambda 表达式, 然后会记住这次执行的结果. 以后所有对 get() 的调用都只会简单地返回以前记住的结果.

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

默认情况下, 延迟加载(lazy)属性的计算是 同步的(synchronized): 属性值只会在唯一一个线程内计算, 但所有线程都将得到同样的属性值. 如果委托的初始化计算不需要同步, 多个线程可以同时执行初始化计算, 那么可以向lazy() 函数传入一个 LazyThreadSafetyMode.PUBLICATION 参数.

如果你确信初期化计算只可能发生在你访问属性的相同线程之内, 那么可以使用 LazyThreadSafetyMode.NONE 模式. 这种模式不会保持线程同步, 因此不会带来这方面的性能损失.

可观察(Observable)属性

Delegates.observable() 函数接受两个参数: 第一个是初始化值, 第二个是属性值变化事件的响应器(handler).

每次你向属性赋值时, 响应器(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" }

如果你希望拦截属性的赋值操作, 并且还能够 否决 赋值操作, 那么不要使用 observable() 函数, 而应该改用 vetoable() 函数. 传递给 vetoable 函数的事件响应器, 会在属性赋值处理执行 之前 被调用.

委托给另一个属性

属性可以将它的 get 和 set 方法委托到另一个属性. 这种委托可以用于顶级属性和类属性 (包括成员属性和扩展属性). 委托属性可以是:

  • 顶级属性

  • 同一个类的成员属性, 或扩展属性

  • 另一个类的成员属性, 或扩展属性

要将一个属性委托到另一个属性, 请在委托名称中使用 :: 限定符, 比如, 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() // 注意: 'oldName: Int' 已废弃. // 请改为使用 'newName' myClass.oldName = 42 println(myClass.newName) // 42 }

将多个属性保存在一个 Map 内

有一种常见的使用场景是将多个属性的值保存在一个 map 之内. 在应用程序解析 JSON, 或者执行某些动态(dynamic)任务时, 经常会出现这样的需求. 这种情况下, 你可以使用 map 实例本身作为属性的委托.

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

上例中, 类的构造器接受一个 map 实例作为参数:

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

委托属性将从这个 map 中读取属性值, 使用属性名称字符串作为 key 值:

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 )) //sampleStart println(user.name) // 打印结果为: "John Doe" println(user.age) // 打印结果为: 25 //sampleEnd }

如果不用只读的 Map, 而改用值可变的 MutableMap, 那么也可以用作 var 属性的委托:

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

局部的委托属性(Local Delegated Property)

你可以将局部变量声明为委托属性. 比如, 你可以为局部变量添加延迟加载的能力:

fun example(computeFoo: () -> Foo) { val memoizedFoo by lazy(computeFoo) if (someCondition && memoizedFoo.isValid()) { memoizedFoo.doSomething() } }

memoizedFoo 变量直到初次访问时才会被计算. 如果 someCondition 的判定结果为 false, 那么 memoizedFoo 变量完全不会被计算.

属性委托的前提条件

对于一个 只读 属性 (val 属性), 它的委托应该提供 getValue 操作符函数, 参数如下:

  • thisRef 参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).

  • property 参数, 类型必须是 KProperty<*>, 或者是它的基类.

getValue() 函数的返回值类型必须与属性类型相同(或者是它的子类型).

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

对于一个 值可变(mutable) 属性(var 属性), 除 getValue 函数之外, 它的委托还必须另外再提供一个 setValue 操作符函数, 参数如下:

  • thisRef 参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).

  • 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 被转换为 val var readWriteResource: Resource by resourceDelegate()

编译器对委托属性的翻译规则

委托属性的底层实现是, 对某些类型的委托属性, Kotlin 编译器会生成辅助属性, 并将目标属性的存取操作委托给这些辅助属性.

比如, 对于属性 prop, 编译器会生成一个隐藏的 prop$delegate 属性, 然后属性 prop 的访问器代码会将存取操作委托给这个新增的属性:

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 属性本身.

对委托属性优化的场景

如果委托属性是以下几种情况, 域成员 $delegate 会被省略:

  • 属性的引用:

    class C<Type> { private var impl: Type = ... var prop: Type by ::impl }
  • 命名对象

    object NamedObject { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ... } val s: String by NamedObject
  • 同一模块内, 带有后端域和默认的 getter 的 final val 属性:

    val impl: ReadOnlyProperty<Any?, String> = ... class A { val s: String by impl }
  • 常数表达式, 枚举值(Enum Entry), this, null. 以下是 this 的例子:

    class A { operator fun getValue(thisRef: Any?, property: KProperty<*>) ... val s by this }

委托到另一个属性时的翻译规则

委托到另一个属性时, 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 // 需要这个方法, 只是为了反射功能 }

控制属性委托的创建逻辑

通过定义一个 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) // 创建委托 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 函数, 然后这个函数立即执行必要的验证.

如果不能对属性与其委托对象的绑定过程进行拦截, 要实现同样的功能, 你就必须在参数中明确地传递属性名称, 这就不太方便了:

// 如果没有 "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) var prop: Type get() = prop$delegate.getValue(this, this::prop) set(value: Type) = prop$delegate.setValue(this, this::prop, value) }

注意, provideDelegate 函数只影响辅助属性的创建, 而不会影响编译产生的属性取值方法和设值方法代码.

使用标准库中的 PropertyDelegateProvider 接口, 可以创建委托提供者(provider), 而不必创建新的类.

val provider = PropertyDelegateProvider { thisRef: Any?, property -> ReadOnlyProperty<Any?, Int> {_, property -> 42 } } val delegate: Int by provider
最终更新: 2024/12/03