Kotlin基础之委托及委托属性

Posted by AlexWan on 2017-07-06

委托

类委托

委托模式是替换继承的较好的设计模式,Kotlin天生支持委托模式,无须任何模板代码。类Derived可以继承Base接口,委托所有public方法给指定对象

interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x)}
}
class Derived(b: Base) : Base by b
fun main(args: Array<String>){
val b = BaseImpl(10)
Derived(b).print() // 输出10
}

Derived的超类列表中的by语句表示b会内部存储在Derived中,编译器会为b生成接口Base所有方法。

复写可以与期望一样生效:编译器使用复写方法替换委托对象中的方法。如果在Derived中添加override fun print() { print("abc") },程序则会输出abc,而不是10

委托属性

有一些普通类型属性,尽管可以在需要时每次手动实现,如果可以一次实现所有将会更好并放入到库中。包括:

  1. 懒属性:只在第一次访问计算的值
  2. 观察属性:监听属性变化的通知
  3. 在map中存储属性,不是每个属性单个一个字段。

为了覆盖这些案例,Kotlin支持委托属性

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

语法为:val/var <property name>: <Type> by <expression>。跟在by之后的表达式为Delegate,因为属性对应的get()set()则被委托给它的getValuesetValue方法。属性委托不用实现任何接口,但是要提供getValuesetValue方法(var类型属性)。比如

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.'")
}
}

当读取委托给实例Delegatep,则会调用DelegategetValue()方法,它的第一个参数为p的对象,第二个参数持有p自己的描述(可以使用它的属性名)。如

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

输出结果

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

类似地,如果给p赋值,则调用setValue()函数,前两个参数一样,第三个为赋予的值

e.p = "NEW"

输出结果

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

从Koltin 1.1开始,可以在函数中或代码块中声明委托属性。

标准委托

Kotlin标准库提供几种有用类型的工厂方法,

懒委托

lazy()函数输入lambda,返回Lazy<T>实例,可以作为实现懒属性的委托:第一次调用执行传入lazy()的lambda,并记住结果,之后调用get(),只返回记住的结果

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

输出结果

computed!
Hello
Hello

懒属性默认为同步赋值:只在一个线程中求值,所有线程都会看到相同的值。如果不需要同步初始化委托,那么多个线程可以同时执行,将LazyThreadSafetyMode.PUBLICATION作为参数传递给lazy函数。如果能够保证初始化在单线程中执行,那么可以使用LazyThreadSafetyMode.NONE模式,但不能保证线程安全和相关开销。

观察者

Delegates.observable()有两个参数:初始值和修改处理Handler。每次给属性赋值时,都会调用handler(在赋值执行之后)。Handler有三个参数:被赋值的属性,旧值和新值

import kotlin.properties.Delegate
class User {
var name: String by Delegate.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。传给vetoable的handler在给属性赋新值前执行。

在Map中存储新值

我们通常使用Map来存储属性值,在应用中很常见,如解析JSON或其他动态的事。可以使用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中取值

println(user.name) // Prints "John Doe"
println(user.age) // Prints 25

对于var属性,使用MutableMap来替换只读的Map

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

局部委托属性(从1.1开始)

从1.1开始可以声明局部变量为委托属性,如:创建局部懒属性

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

memoizedFoo属性只在第一次访问时进行计算,如果someCondition失败,则不会计算变量。

属性委托要求

下面总结委托对象的要求

  1. 对于只读属性(val),委托对象提供名为getValue的函数,并带有下面几个参数

    • thisRef - 类型必须与属性对象的超类一致(对于扩展属性:扩展的类型)
    • property - 必须为KProperty<*>或其子类
    • 函数返回值必须与属性或其子类属性一致
  2. 对于可变属性(var), 委托对象需要额外提供名为setValue的函数,并带有下面几个参数

    • thisRef - 必须与getValue()一样
    • property - 与getValue()一样
    • 新值 - 类型必须与属性对象的超类一致

getValue()和(或)setValue()可能会作为委托对象的成员函数或扩展函数,当需要委托属性给对象时(没有这些函数)时,扩展函数会比较方便。这两种函数都需要使用operator关键字来标记。

委托类可以实现ReadOnlyPropertyReadWriteProperty其中一个接口,包含需要的operator方法。Kotlin标准库声明了这些接口。

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编译器会生成一个辅助属性,并委托给为委托属性。例如:prop会生成prop$delegate隐藏属性,访问者的代码只委托给了附加的属性:

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)
}

Koltin编译器提供有关prop的所有的必要信息:第一个参数this是外部类C的引用,this::propKProperty类型的反射对象,描述prop

this::prop语法表示直接绑定调用代码中的应用,在Kotlin1.1后可用

提供委托(从1.1开始)

定义provideDelegate操作函数,可以继承创建对象给委托属性的逻辑。如果给在by右边使用的对象,定了成员函数或扩展函数provideDelegate,则创建委托属性时调用这个函数。

使用provideDelegate的一种情况就是:在创建属性时,检查属性一致性,不仅仅是在gettersetter中。

如:希望在绑定前,检查属性名,可以这样做

class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// create delegate
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}

provideDelegate参数与getValue一样

  • thisRef - 类型必须与属性对象的超类一致(对于扩展属性:扩展的类型)
  • property - 必须为KProperty<*>或其子类

在创建MyUI实例时,调用每个属性的provideDelegate方法,立即执行必要的验证

不能够拦截property和委托类的绑定操作,可以显性传入属性名来达到这个效果。

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方法用来初始化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)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}

provideDelegate方法只影响辅助属性的创建,不影响生成的getter和setter代码。