委托属性
Kotlin官网:Classes and Objects-Delegated Properties
对于那些频繁使用的情形:
- 延迟初始化属性:值在第一次使用时计算,之后被缓存
- 被观察属性:在被改变时通知监听者
- 在map中存储的属性
对于以上情形,相比于每次都写重复的代码,Kotlin提供了原生的实现:
class Example {
var p: String by Delegate()
}
格式为:
val/var 属性名: 类型 by 表达式
by后面的表达式是委托代表,属相的get和set会委托到代表的setValue和getValue方法。
委托代表不要求必须实现某个接口,但必须提供getValue函数,如果是var可变变量委托,还需提供setValue函数。
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属性时,实际是调用了委托代表的getValue和setValue函数
访问p属性值:
val e = Example()
println(e.p)
此时相当于调用Delegate的getValue函数,打印出:
Example@33a17727, thank you for delegating ‘p’ to me!
为p属性赋值:
e.p = "NEW"
此时相当于调用Delegate的setValue函数,打印出:
NEW has been assigned to ‘p’ in Example@33a17727.
Kotlin1.1后委托属性可以在函数和代码块中使用,不再必须为类的成员。
标准库
Kotlin标准库中提供了几个常用委托代表的实现
延迟初始化lazy
使用lazy()函数返回Lazy<T>实例作为代表。
lambda会在第一次访问属性时执行,并将返回值缓存,之后再次调用属性只会返回缓存的返回值,不再执行lambda中的代码
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}
打印结果:
computed!
Hello
Hello
computed!和第一个Hello是第一次执行时,完整执行了lambda中的代码,并将返回值Hello缓存了起来,之后再调用lazyValue属性会直接返回缓存的Hello,不再执行lambda内容,所以第二次println(lazyValue)只打印了第二个hello,computed!不再出现。
lazy默认为同步的实现,如果想手动干预,可以给lazy函数传参。
val lazyValue: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
println("computed!")
"Hello"
}
这样lazy就允许在多个线程中同时执行。
如果自己确保单线程初始化,可以使用LazyThreadSafetyMode.NONE
,此时不保证线程安全,并节省相应确保线程安全的开支。
观察
使用Delegates.observable()
,包含两个参数:初始值和变化处理(handler)。
每次属相被改变时,handler都会被调用。
handler有3个参数:被改变的属性,旧值,新值
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
打印:
<no name> -> first
first -> second
如果想在值改变前拦截操作,使用vetoable()
替换observable()
,handler会在属性被写入新值前调用。
Map中保存属性
使用map作为属性的委托代表,属性的名字为在map中的key。
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就是属性的名字:
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
对于可变属性var,可以使用可变map委托:
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
局部委托属性(1.1以后)
可以在局部声明委托属性,例如将局部变量委托lazy
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
对于上例,memoizedFoo
只在第一次被调用时执行lazy的内容,如果someCondition为false,&&后不执行,也就是memoizedFoo不会被访问,computeFoo不会被执行。
属性委托的要求
对于只读属性(val),委托代表必须有getValue函数,包含如下两个参数:
- thisRef:类型是属性所有者或其父类,扩展方法为被扩展类型
- property:KProperty<*>及其父类
getValue的返回值必须是声明的属性的类型或其子类。
对于可变属性(var),还必须有setValue函数,包含如下3个参数:
- thisRef:和setValue相同
- property:和setValue相同
- new value:属性的类型或其父类
getValue和setValue必须是委托代表类的成员函数或者扩展函数。
扩展函数一般已有用于代表对象,但没有要求的getValue或setValue函数。
setValue和getValue都要有operator关键字。
Kotlin标准库中提供ReadOnlyProperty和ReadWriteProperty两个接口,内部包含需要的函数,并有operator,可以直接实现使用:
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
转换规则
Kotlin编译器会为委托属性生成一个辅助属性,属性的访问器会被委托到辅助属性。举个例子:
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)
}
上例中,上半部分的代码是写的,下半部分的代码是编译器转换后的,编译器为prop属性生成了一个prop$delegate辅助属性,这个辅助属性指向委托代表,prop属性的访问器实际调用了辅助属性的getValue和setValue函数,this指向C类的当前实例,this::prop是prop属性的一个KProperty类型的反射对象。
this::prop这种形式在Kotlin1.1后支持。
提供代表(Kotlin1.1后支持)
通过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:KProperty<*>及其父类
在上例中,创建MyUI类的实例时,每个属性都会调用provideDelegate。
如果没有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被调用来初始化生成的辅助属性,和没有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)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}
可以看到声明provideDelegate函数只会改变辅助属性初始化,并不会影响生成的访问器。