Kotlin知识归纳(十) —— 委托

2,388 阅读9分钟

前序

      委托,对于很多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)
    }

}

委托属性

委托属性就是将属性的访问器(getset)委托给一个符合属性委托约定规则的对象。

      委托属性和类委托不同,委托属性更像是给属性找代理。 委托属性同样是利用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官方库中提供 ReadOnlyPropertyReadWriteProperty 接口,方便开发者实现这些接口来提供正确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的委托属性如何使用后,还需要深入了解下委托属性的源码:

NotNullVar:Delegates.notNull()延迟初始化的委托类

Delegates:Delegates作为一个对象声明存在,里面拥有3个非常熟悉的方法:notNull()observablevetoable

ObservablePropertyObservableProperty系统定义的委托类,observablevetoable返回该委托类的匿名对象。

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函数。ObservablePropertygetValue()会先获取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、创建一个继承自ObservablePropertyPerson?special?inlined$observable$1类,因为Delegates.observable()是返回一个匿名的ObservableProperty对象。
  • 2、Person类中定义了一个name$delegate属性,该属性指向name属性的代理对象,即Person?special?inlined$observable$1类的对象。
  • 3、Person类中name属性会转换为getName()setName()
  • 4、name属性的getset方法的内部调用name$delegate相应的setValue()getValue()
  • 5、KProperty数组中会保存通过Kotlin反射得到的Personr类中的name属性的信息。在调用name$delegatesetValue()getValue()时,将这些信息作为参数传递进去。

幕后字段与幕后属性

      你看完反编译的Java源码后,或许会发现一个问题:为什么Kotlin中Personname属性并没有在Java的Person中被定义,只实现了该属性的getset方法。

      这其中涉及到Kotlin的幕后字段的问题, Kotlin 什么是幕后字段? 中讲得很清楚:

只有拥有幕后字段的属性转换成Java代码时,才有对应的Java变量。

Kotlin属性拥有幕后字段需要满足以下条件之一:

  • 使用默认 getter / setter 的属性,一定有幕后字段。对于 var 属性来说,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段;
  • 在自定义 getter / setter 中使用了 field 的属性

      所以也就能理解,为什么扩展属性不能使用 field,因为扩展属性并不能真的在该类中添加新的属性,不能具有幕后字段。而且委托属性中,该属性的getset方法内部都是调用代理对象的getValue()setValue(),并没有使用 field ,且都不是使用默认的getset方法。

总结

  • 类委托可以很方便的实现装饰设计模式,开发者只用关心需要扩展的方法。
  • 委托属性就是将该属性的setget交由 代理对象 的setValuegetValue来处理。
  • 委托属性也是一种 约定 。setValuegetValue都需带有operator关键字修饰。
  • Kotlin标准库提供 ReadOnlyPropertyReadWriteProperty 接口,方便开发者实现这些接口来提供正确getValue()方法 和 setValue()方法。
  • val属性可以借助 委托属性 进行延迟初始化,使用lazy()设置初始化流程,并自动返回代理对象。
  • Delegates.observable()能在 被委托的属性 改变时接收到通知,有点类似ACC的LiveData
  • Delegates.vetoable()能在 被委托的属性 改变前接收通知,并能决定该属性赋不赋予新值。
  • Delegates.notNull()可以用作任何类型的var变量进行 延迟初始化
  • 只有拥有幕后字段的属性转换成Java代码时,才有对应的Java变量。

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射