Kotlin知识归纳(十) —— 委托
前序
委托,对于很多Java开发者来说都会一面蒙蔽,我也不例外。委托,维基百科的解释是:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。这好像有一点代理的味道(*゜ー゜*)。Kotlin中委托分为类委托和委托属性。
类委托
在解释类委托之前,需要先了解一波装饰设计模式。装饰设计模式的核心思想是:
在不使用继承的情况下,扩展一个对象的功能,使该对象变得更加强大。
通常套路是:创建一个新类,新类实现与原始类一样的接口,并将原来的类的实例作为作为一个字段保存,与原始类拥有同样的行为(方法)。一部分行为(方法)与原始类保持一致(即直接调用原始类的行为(方法)),还有一部分行为(方法)在原始类的行为(方法)基础上进行扩展。
装饰设计模式的缺点是需要较多的样板代码,显得比较啰嗦。例如:最原始的装饰类需要实现接口的全部方法,并在这些方法中调用原始类对象对应的方法。
class CustomList<T>(
val innerList:MutableList<T> = ArrayList<T>()):MutableCollection<T> {
override val size: Int = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun add(element: T): Boolean = innerList.add(element)
override fun addAll(elements: Collection<T>): Boolean = innerList.addAll(elements)
override fun clear() = innerList.clear()
override fun iterator(): MutableIterator<T> = innerList.iterator()
override fun remove(element: T): Boolean = innerList.remove(element)
override fun removeAll(elements: Collection<T>): Boolean = innerList.removeAll(elements)
override fun retainAll(elements: Collection<T>): Boolean = innerList.retainAll(elements)
}
但Kotlin将委托作为一个语言级别的功能进行头等支持。可以利用by
关键字,将新类的接口实现委托给原始类,编译器会为新类自动生成接口方法,并默认返回原始类对应的具体实现。然后我们重载需要扩展的方法。
class CustomList<T>(
val innerList:MutableList<T> = ArrayList<T>()):MutableCollection<T> by innerList{
override fun add(element: T): Boolean {
println("CustomList add element")
innerList.add(element)
}
}
委托属性
委托属性就是将属性的访问器(
get
和set
)委托给一个符合属性委托约定规则的对象的。
委托属性和类委托不同,委托属性更像是给属性找代理。 委托属性同样是利用by
关键字,将属性委托给代理对象。属性的代理对象不必实现任何的接口,但是需要提供一个 getValue()
函数与 setValue()
函数(仅限 var
属性)。例如:
class Person{
var name:String by Delegate()
}
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "kotlin"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
}
}
属性 name
将自己的set
/get
方法委托给了Delegate对象的getValue()
和 setValue()
。在getValue()
和 setValue()
中都有operator
修饰,意味着委托属性也是依赖于约定的功能。像其他约定的函数一样,getValue()
和 setValue()
可以是成员函数,也可以是扩展函数。
Kotlin官方库中提供 ReadOnlyProperty
或 ReadWriteProperty
接口,方便开发者实现这些接口来提供正确的 getValue()
方法 和 setValue()
方法。
public interface ReadOnlyProperty<in R, out T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
}
public interface ReadWriteProperty<in R, T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
使用委托属性
惰性初始化
当需要进行属性延迟初始化时,往往会想到使用lateinit var
进行延迟初始化。但那是对于var
变量,即可变变量,但对于val
变量呢?可以使用支持属性来实现惰性初始化:
class Person{
//真正存储邮箱列表的对象
private var _emails:List<Email>? = null
//对外暴露的邮箱列表对象
val emails:List<Email>
get() {
if ( _emails == null){
_emails = ArrayList<Email>()
}
return _emails!!
}
}
提供一个"隐藏"属性_emails
用来存储真正的值,而另一个属性emails
用来提供属性的读取访问。_emails
是可变可空,emails
不可变不可空,当你访问emails
时,才初始化_emails
变量,并返回_emails
对象,达到对val
对象延迟初始化的目的。
但这种方案在需要多个惰性属性时,就显得很啰嗦了,而且他并不是线程安全的。Kotlin提供了更加便捷的解决方案:委托属性,并使用标准库函数lazy
返回代理对象。
class Person{
val email:List<Email> by lazy {
ArrayList<Email>()
}
}
lazy
函数接收初始化该值操作的lambda,并返回一个具有getValue()
方法的代理对象,并配合by
关键字将属性委托给lazy
函数返回的代理对象。lazy
函数是线程安全的,不用担心异步的问题。
属性改变的通知
当一个对象的属性需要更改时得到通知,最原始的办法就是,重写set方法,在set方法中设置处理属性改变的逻辑。手工实现属性修改的通知:
class Person(name:String,age:Int){
var age :Int = age
set(newValue) {
val oldValue = field
field = newValue
//监听值改变(或使用Listener对象)
valueChangeListener("age",oldValue,newValue)
}
var name :String = name
set(newValue) {
val oldValue = field
field = newValue
//监听值改变
valueChangeListener("name",oldValue,newValue)
}
fun <T> valueChangeListener(fieldName:String,oldValue:T,newValue:T){
println("$fieldName oldValue = $oldValue newValue = $newValue")
}
}
但这种方案在跟惰性初始化最开始的例子类似,当需要监听多个属性时,代码冗长且啰嗦。我们可以像惰性初始化一样,使用委托属性实现:
class Person(name:String,age:Int){
var age:Int by PropertyObservable(age){ property, oldValue, newValue ->
}
var name:String by PropertyObservable(name){ property, oldValue, newValue ->
}
}
//委托类
class PropertyObservable<T>(var initValue:T,
val observer:(property: KProperty<*>, oldValue: T, newValue: T) -> Unit): ReadWriteProperty<Person, T> {
override fun getValue(thisRef: Person, property: KProperty<*>): T {
return initValue;
}
override fun setValue(thisRef: Person, property: KProperty<*>, newValue: T) {
val oldeValue = initValue
initValue = newValue
//监听值改变(或使用Listener对象)
observer(property,oldeValue,newValue)
}
}
定义委托类,通过委托属性"接管"该属性的get
/set
,并提供初始值,以及属性被修改时的处理逻辑。大大简化属性设置监听的代码。
但Kotlin在标准库中已经为我们提供了Delegates.observable()
方法,大大方便我们使用委托属性对属性的修改进行监听,像我们自定义的委托类一样,该方法接受属性的初始化值,以及属性变化时的处理逻辑:
class Person(name:String,age:Int){
var age:Int by Delegates.observable(age){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
var name:String by Delegates.observable(name){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
}
Kotlin在标准库中提供了一个类似Delegates.observable()
的方法:Delegates.vetoable()
。但会在属性被赋新值生效之前会传递给 Delegates.vetoable()
进行处理,依据Delegates.vetoable()
的返回的布尔值判断要不要赋新值。
第三种延迟初始化
之前已经知道,var
属性需要延迟初始化时,可以使用lateinit
关键字,val
属性需要延迟初始化时,可以使用委托属性 + lazy()
函数的方法。但lateinit
关键字的延迟处理仅对引用类型有用,对基本数据类型无效,当需要对基本数据类型进行延迟初始化怎么办呢?Kotlin通过委托属性提供另一种延迟初始化的方式:Delegates.notNull()
var num:Int by Delegates.notNull()
虽然Kotlin提供了延迟初始化的方式,使开发者不用强制在构造函数中初始化(例如Activity
中在onCreate
中初始化),但对于延迟初始化的值,必须确保其被初始化,否则将会像Java空指针一样,抛出异常。
方式 | 适用类型 |
---|---|
lateinit | 引用类型 |
Delegates.notNull() | 基本数据类型、引用类型 |
转换规则
每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。例如:对于属性 name
,编译器会生成隐藏属性 name$delegate,而属性 name
访问器的代码委托给隐藏属性的getValue()/setValue()。
class Person{
var name:String by MyDelegate()
}
编译器生成以下代码:
class Person{
private val name$delegate = MyDelegate()
var name:String
get() = name$delegate.getValue(this,<property>)
set(value:String) = name$delegate.setValue(this,<property>,value)
}
- thisRef表示持有该委托属性的对象
- property KProperty<*> 类型或是它的父类,属性的描述。(可获取属性的名称等)
- value 属性的新值
源码阅读
掌握了Kotllin的委托属性如何使用后,还需要深入了解下委托属性的源码:
imageNotNullVar:Delegates.notNull()
延迟初始化的委托类
Delegates:Delegates
作为一个对象声明存在,里面拥有3个非常熟悉的方法:notNull()
、observable
和vetoable
。
ObservableProperty:ObservableProperty
系统定义的委托类,observable
和vetoable
返回该委托类的匿名对象。
Delegates.notNull()
Delegates.notNull()
直接返回NotNullVar
对象作为委托属性的代理对象。
#Delegates.kt
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
#Delegates.kt
private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
从源码中可以看到其内部实现与之前我们使用的支持属性是一样的原理,但他提供了getValue()
和setValue()
,使Delegates.notNull()
可以代理var
属性。
Delegates.observable()
Delegates.observable()
和Delegates.vetoable()
一样,都是直接返回ObservableProperty的匿名对象。但Delegates.observable()
重载afterChange
函数,并在afterChange
函数中执行Delegates.observable()
接收的lambda。ObservableProperty#setValue()
在对属性赋新值后,将旧值和新值作为参数执行afterChange
函数。
#Delegates.kt
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}
#ObservableProperty.kt
protected open fun afterChange(property: KProperty<*>, oldValue: T, newValue: T): Unit {}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
Delegates.vetoable()
Delegates.vetoable()
与Delegates.observable()
非常相似,只是重载的函数不一致,Delegates.vetoable()
重载beforeChange
函数。ObservableProperty
的getValue()
会先获取beforeChange
函数的返回值(默认是true),判断是否继续执行赋值操作。所以这就是Delegates.vetoable()
的不同的地方。
#Delegates.kt
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty<Any?, T> =
object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}
#ObservableProperty.kt
protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = this.value
//如果beforeChange返回false,则直接返回函数,不赋值
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
委托属性的原理
想要更深层次的了解Kotlin的委托,最好的办法就是将其转换成Java代码进行研究。
#daqiKotlin.kt
class Person{
var name:String by Delegates.observable("daqi"){ property, oldValue, newValue ->
println("${property.name} oldValue = $oldValue newValue = $newValue")
}
}
反编译后的Java代码:
public final class Person$$special$$inlined$observable$1 extends ObservableProperty {
// $FF: synthetic field
final Object $initialValue;
public Person$$special$$inlined$observable$1(Object $captured_local_variable$1, Object $super_call_param$2) {
super($super_call_param$2);
this.$initialValue = $captured_local_variable$1;
}
protected void afterChange(@NotNull KProperty property, Object oldValue, Object newValue) {
Intrinsics.checkParameterIsNotNull(property, "property");
String newValue = (String)newValue;
String oldValue = (String)oldValue;
int var7 = false;
String var8 = property.getName() + " oldValue = " + oldValue + " newValue = " + newValue;
System.out.println(var8);
}
}
public final class Person {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;"))};
@NotNull
private final ReadWriteProperty name$delegate;
@NotNull
public final String getName() {
return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
}
public Person() {
Delegates var1 = Delegates.INSTANCE;
Object initialValue$iv = "daqi";
ReadWriteProperty var5 = (ReadWriteProperty)(new Person$$special$$inlined$observable$1(initialValue$iv, initialValue$iv));
this.name$delegate = var5;
}
}
- 1、创建一个继承自
ObservableProperty
的Person$$special$$inlined$observable$1
类,因为Delegates.observable()
是返回一个匿名的ObservableProperty
对象。 - 2、
Person
类中定义了一个name$delegate
属性,该属性指向name
属性的代理对象,即Person$$special$$inlined$observable$1
类的对象。 - 3、
Person
类中name
属性会转换为getName()
和setName()
。 - 4、
name
属性的get
和set
方法的内部调用name$delegate
相应的setValue()
和getValue()
。 - 5、KProperty数组中会保存通过Kotlin反射得到的Personr类中的name属性的信息。在调用
name$delegate
的setValue()
和getValue()
时,将这些信息作为参数传递进去。
幕后字段与幕后属性
你看完反编译的Java源码后,或许会发现一个问题:为什么Kotlin中Person
的name
属性并没有在Java的Person
中被定义,只实现了该属性的get
和set
方法。
这其中涉及到Kotlin的幕后字段的问题, Kotlin 什么是幕后字段? 中讲得很清楚:
只有拥有幕后字段的属性转换成Java代码时,才有对应的Java变量。
Kotlin属性拥有幕后字段需要满足以下条件之一:
- 使用默认
getter
/setter
的属性,一定有幕后字段。对于var
属性来说,只要getter
/setter
中有一个使用默认实现,就会生成幕后字段; - 在自定义
getter
/setter
中使用了field
的属性
所以也就能理解,为什么扩展属性不能使用 field
,因为扩展属性并不能真的在该类中添加新的属性,不能具有幕后字段。而且委托属性中,该属性的get
和set
方法内部都是调用代理对象的getValue()
或setValue()
,并没有使用 field
,且都不是使用默认的get
和set
方法。
总结
- 类委托可以很方便的实现装饰设计模式,开发者只用关心需要扩展的方法。
- 委托属性就是将该属性的
set
和get
交由 代理对象 的setValue
和getValue
来处理。 - 委托属性也是一种 约定 。
setValue
和getValue
都需带有operator
关键字修饰。 - Kotlin标准库提供
ReadOnlyProperty
或ReadWriteProperty
接口,方便开发者实现这些接口来提供正确的getValue()
方法 和setValue()
方法。 -
val
属性可以借助 委托属性 进行延迟初始化,使用lazy()
设置初始化流程,并自动返回代理对象。 -
Delegates.observable()
能在 被委托的属性 改变时接收到通知,有点类似ACC的LiveData -
Delegates.vetoable()
能在 被委托的属性 改变前接收通知,并能决定该属性赋不赋予新值。 -
Delegates.notNull()
可以用作任何类型的var
变量进行 延迟初始化 - 只有拥有幕后字段的属性转换成Java代码时,才有对应的Java变量。
参考资料:
- 《Kotlin实战》
- Kotlin官网