Kotlin学习笔记3-13 类和对象-委托属性

委托属性

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函数只会改变辅助属性初始化,并不会影响生成的访问器。