委托属性
有许多非常具有共性的属性, 虽然你可以在每个需要这些属性的类中手工地实现它们, 但是, 如果能够只实现一次, 然后将它放在库中, 供所有需要者重复使用, 那将会很有帮助. 例如:
延迟加载(lazy) 属性: 属性值只在初次访问时才会计算.
可观察(observable) 属性: 属性发生变化时, 监听器会收到通知.
将多个属性保存在一个 map 内, 而不是将每个属性保存在一个独立的域内.
为了解决这些问题(以及其它问题), Kotlin 允许 委托属性(delegated property):
委托属性的语法是: val/var <property name>: <Type> by <expression>
. 其中 by
关键字之后的表达式就是 委托, 属性的 get()
方法(以及 set()
方法) 将被委托给这个对象的 getValue()
和 setValue()
方法. 属性委托不必实现接口, 但必须提供 getValue()
函数(对于 var
属性, 还需要 setValue()
函数).
示例:
如果属性 p
委托给一个 Delegate
的实例, 那么当你读取属性值时, 就会调用到 Delegate
的 getValue()
函数. 此时函数收到的第一个参数将是你访问的属性 p
所属的对象实例, 第二个参数将是 p
属性本身的描述信息(比如, 你可以从这里得到属性名称).
这段代码的打印结果将是:
类似的, 当你向属性 p
赋值时, 将会调用到 setValue()
函数. 这个函数收到的前两个参数与 getValue()
函数相同, 第三个参数将是即将赋给属性的新值:
这段代码的打印结果将是:
对属性委托对象的要求, 详细的说明请参见下文.
你可以在函数内, 或者一个代码段内定义委托属性, 委托属性不一定需要是类的成员. 参见 示例.
标准委托
Kotlin 标准库中提供了一些工厂方法, 可以实现几种很有用的委托.
延迟加载(Lazy)属性
lazy()
是一个函数, 接受一个 Lambda 表达式作为参数, 返回一个 Lazy<T>
类型的实例, 这个实例可以作为一个委托, 实现延迟加载(lazy)属性. 第一次调用 get()
时, 将会执行 lazy()
函数受到的 Lambda 表达式, 然后会记住这次执行的结果. 以后所有对 get()
的调用都只会简单地返回以前记住的结果.
默认情况下, 延迟加载(lazy)属性的计算是 同步的(synchronized): 属性值只会在唯一一个线程内计算, 但所有线程都将得到同样的属性值. 如果委托的初始化计算不需要同步, 多个线程可以同时执行初始化计算, 那么可以向lazy()
函数传入一个 LazyThreadSafetyMode.PUBLICATION
参数.
如果你确信初期化计算只可能发生在你访问属性的相同线程之内, 那么可以使用 LazyThreadSafetyMode.NONE
模式. 这种模式不会保持线程同步, 因此不会带来这方面的性能损失.
可观察(Observable)属性
Delegates.observable()
函数接受两个参数: 第一个是初始化值, 第二个是属性值变化事件的响应器(handler).
每次你向属性赋值时, 响应器(handler)都会被调用(在属性赋值处理完成 之后). 响应器收到三个参数: 被赋值的属性, 赋值前的旧属性值, 以及赋值后的新属性值:
如果你希望拦截属性的赋值操作, 并且还能够 否决 赋值操作, 那么不要使用 observable()
函数, 而应该改用 vetoable()
函数. 传递给 vetoable
函数的事件响应器, 会在属性赋值处理执行 之前 被调用.
委托给另一个属性
属性可以将它的 get 和 set 方法委托到另一个属性. 这种委托可以用于顶级属性和类属性 (包括成员属性和扩展属性). 委托属性可以是:
顶级属性
同一个类的成员属性, 或扩展属性
另一个类的成员属性, 或扩展属性
要将一个属性委托到另一个属性, 请在委托名称中使用 ::
限定符, 比如, this::delegate
或 MyClass::delegate
.
这种功能的用途是, 比如, 如果你希望修改属性名称, 同时又保持向后兼容: 这时可以引入一个新的属性, 将旧的属性标注 @Deprecated
注解, 然后将它的实现委托给新属性.
将多个属性保存在一个 Map 内
有一种常见的使用场景是将多个属性的值保存在一个 map 之内. 在应用程序解析 JSON, 或者执行某些动态(dynamic)任务时, 经常会出现这样的需求. 这种情况下, 你可以使用 map 实例本身作为属性的委托.
上例中, 类的构造器接受一个 map 实例作为参数:
委托属性将从这个 map 中读取属性值, 使用属性名称字符串作为 key 值:
如果不用只读的 Map
, 而改用值可变的 MutableMap
, 那么也可以用作 var
属性的委托:
局部的委托属性(Local Delegated Property)
你可以将局部变量声明为委托属性. 比如, 你可以为局部变量添加延迟加载的能力:
memoizedFoo
变量直到初次访问时才会被计算. 如果 someCondition
的判定结果为 false, 那么 memoizedFoo
变量完全不会被计算.
属性委托的前提条件
对于一个 只读 属性 (val
属性), 它的委托应该提供 getValue
操作符函数, 参数如下:
thisRef
参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).property
参数, 类型必须是KProperty<*>
, 或者是它的基类.
getValue()
函数的返回值类型必须与属性类型相同(或者是它的子类型).
对于一个 值可变(mutable) 属性(var
属性), 除 getValue
函数之外, 它的委托还必须另外再提供一个 setValue
操作符函数, 参数如下:
thisRef
参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类).property
参数, 类型必须是KProperty<*>
, 或者是它的基类.value
参数, 类型必须与属性类型相同(或者是它的基类).
getValue()
和 setValue()
函数可以是委托类的成员函数, 也可以是它的扩展函数. 如果你需要将属性委托给一个对象, 而这个对象本来没有提供这些函数, 这时使用扩展函数会更便利一些. 这两个函数都需要标记为 operator
.
通过使用 Kotlin 标准库中的 ReadOnlyProperty
和 ReadWriteProperty
接口, 可以用匿名对象的方式创建委托, 而不必创建新类. 这些接口提供了需要的方法: getValue()
声明在 ReadOnlyProperty
接口中; ReadWriteProperty
继承了这个接口, 然后增加了 setValue()
方法. 因此在需要 ReadOnlyProperty
的地方, 你也可以使用 ReadWriteProperty
.
编译器对委托属性的翻译规则
委托属性的底层实现是, 对某些类型的委托属性, Kotlin 编译器会生成辅助属性, 并将目标属性的存取操作委托给这些辅助属性.
比如, 对于属性 prop
, 编译器会生成一个隐藏的 prop$delegate
属性, 然后属性 prop
的访问器代码会将存取操作委托给这个新增的属性:
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
. 这样的代码优化可以节约内存.
示例:
prop
变量的属性访问器直接调用 impl
变量, 跳过被代理属性的 getValue
和 setValue
操作, 因此也不需要 KProperty
引用对象.
对于上面的代码, 编译器生成以下代码:
控制属性委托的创建逻辑
通过定义一个 provideDelegate
操作符, 你可以控制属性委托对象的创建逻辑. 如果在 by
右侧的对象中定义了名为 provideDelegate
的成员函数或扩展函数, 那么这个函数将被调用, 用来创建属性委托对象的实例.
provideDelegate
的一种可能的使用场景, 是在属性初始化时检查属性的一致性.
比如, 如果要在(属性与其委托对象)绑定之前检查属性名称, 你可以编写这样的代码:
provideDelegate
函数的参数与 getValue
相同:
thisRef
参数, 类型必须与 属性所属的类 相同, 或者是它的基类 (对于扩展属性, 参数类型必须与被扩展的类型相同, 或者是它的基类);property
参数, 类型必须是KProperty<*>
, 或者是它的基类.
在 MyUI
的实例创建过程中, 将会对各个属性调用 provideDelegate
函数, 然后这个函数立即执行必要的验证.
如果不能对属性与其委托对象的绑定过程进行拦截, 要实现同样的功能, 你就必须在参数中明确地传递属性名称, 这就不太方便了:
在编译器生成的代码中, 会调用 provideDelegate
方法, 用来初始化辅助属性 prop$delegate
. 请看属性声明 val prop: Type by MyDelegate()
对应的生成代码, 并和上例 (没有 provideDelegate
方法的情况) 的代码对比以下:
注意, provideDelegate
函数只影响辅助属性的创建, 而不会影响编译产生的属性取值方法和设值方法代码.
使用标准库中的 PropertyDelegateProvider
接口, 可以创建委托提供者(provider), 而不必创建新的类.