Kotlin Delegated Properties

Delegated Properties,代理属性。顾名思义,就是我们在同这一个属性进行交互的时候并不操作属性本身,而是操作属性的代理类对象
如果一个属性被按照以下的句法写成:

1
val/var <property name>: <Type> by <expression>

那么这一个属性就是 代理类对象by 后面的 expression 就是代理类对象。所有对属性 get() 或者 set() 方法的调用都会被转换成对代理类对象 getValue()setValue() 方法的调用。

Basics

Delegated Properties 的实现原理是属性的 get() 和 set() 方法都转换给了代理类执行。这一转换是在编译期 kotlinc 自动实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
//假设我们写了一个类C,对属性prop 使用MyDelegate() 的代理。
class C {
var prop: Type by MyDelegate()
}

//经过 kotlinc 的编译,出来的.class文件中属性 prop 的结构会是这样
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的 get() 和 set() 方法都是由代理类对象 prop$delegate的 getValue() 和 setValue() 方法来执行。

标准代理属性

代理类对象有三个标准的使用场景:

  • lazy 懒加载
  • observable/vetoable 观察者模式
  • map 映射属性到Map
lazy
1
2
3
4
//lazy方法声明在kotlin.Lazy.kt当中
val lazyValue: String by lazy {
"Hello"
}

通过这样的写法,lazyValue 便会拥有一个 Lazy 类型的代理类对象。lazyValue 会在自己的 get()方法第一次被调用时才执行初始化操作。 而lazy 后传给 Lazy 的lambda函数便是初始化函数;初始化函数的返回值便是 lazyValue 的值。
lazyValue初始化lambda函数 只会执行一次,后续对 lazyValueget() 方法调用会直接返回初始化lambda函数 中最后返回的值。
lazy 的API能够让开发者在需要用到某个属性的时候再根据当时的具体信息去确定该属性具体的值。

observable/vetoable 观察者模式

这两个都是 kotlin.properties.Delegates 中的 inline函数,可以针对某个属性,在属性变化时对属性进行 “拦截”vetoable 是属性变化前进行拦截;observable 是属性变化后进行拦截。

例子:

1
2
3
4
5
6
7
8
9
10
var observerName: String by Delegates.observable("<no name>") {
prop, old, new -> //prop 是 observerName本身
println("$old -> $new")
}

var vetoName: String by Delegates.vetoable("<no name>") {
prop, old, new -> //prop 是 vetoName本身
println("$old -> $new")
false //false 代表prop 依旧停留在旧的 old值上,不会改变为新的 new值。
}

对一个属性的变化进行监听在日常的开发需求中非常常见,如果你是一位Android App工程师,必定会知道官方还为此功能出了一个Library —— LiveData。比起LiveData, Kotlin stdlib中内嵌的 observable/vetoable 模式不仅仅让你只需要一行代码就实现对属性变化的监听,还能够对变化进行拦截。

map

在App的开发中,尤其是基于HTTP进行网络内容交换的开发中,JSON - DataEntity 之间的转换需求非常频繁,同时 JSON 经过转换之后的数据结构基本都说Map类型,因此Kotlin就出了一个 map 的代理类API,帮助轻松完成 Map 类型到 DataEntity 之间的转换。

假设声明了一个实体类,实体类的属性使用 “by” 来指明需要映射到哪个 map上

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

这个时候只需要往构造器里传入一个Map类型的数据结构,Map中的值就会被自动映射到对应的属性上。

1
2
3
4
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

你一定会想,这里是一个DataEntity实例 mapping 一个 Map, 可否一个DataEntity mapping 多个Map呢?
好消息是,可以!下面这个例子将为你展示如果将两个Map中的Key映射到同一个DataEntity当中。

1
2
3
4
5
6
7
8
9
//DataEntity的声明
class C(map1: Map<String, Any?>, map2: Map<String, Any?>) {
val name1: String by map1
val name2: String by map2
val data: String by map2
}

//调用mapOf 来创建Map,并调用C的构造器
val c = C(mapOf("name1" to "John", "name2" to "Jason"), mapOf("name2" to "Hello", "data" to "World"))

然后你发现

1
2
3
name1 = John
name2 = Hello //不是 "Jason" 的原因是通过 "by" 关键字,我们要求name2去map2中找Mapping
data = World

自定义代理属性

你已经知道如何使用标准代理属性了,下面将展示如何创建一个自定义代理属性
假设我们要为类C创建一个代理类 CDelegate,如果C中的属性使用了代理,则该属性的get()方法将会代理到代理类中

  • 首先需要让代理类 CDelegate 继承自 ReadOnlyProperty,同时实现其中的 getValue() 方法,后面被代理对象的 get() 方法执行时将会被转发到 getValue() 中执行。
  • 同时声明一个用 operator 修饰符修饰的 provideDelegate 方法,在后面创建 CDelegate 的实例对象时,provideDelegate 方法会被立即调用,并为被代理对象创建代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// NOTE: 在本例中,代理对象只是 CDelegate,你可以通过返回不同的代理类对象从而代理到不同的代理中。
class CDelegate : ReadOnlyProperty<C, String> {
operator fun provideDelegate (thisRef: C, property: KProperty<*>) :ReadOnlyProperty<C, kotlin.String> {
Log.d(Const.TAG, "Calling providerDelegate()")
//可以在创建前进行一些检查
return CDelegate()
}

override fun getValue(thisRef: C, property: KProperty<*>): String {
//在这里进行代理 get() 的计算和处理
Log.d(Const.TAG, "Calling getValue()")
return "End"
}
}

然后在使用时,只需要简单在需要被代理的属性后使用 by 修饰符来声明代理类对象即可:

1
2
3
4
5
6
7
class C {
val c by CDelegate()

fun call() {
Log.d(Const.TAG, "c value is: $c")
}
}

在创建完 c 之后,所有对其 get() 方法的调用都会被代理到 CDelegategetValue() 方法当中。