问:聊聊你对 kotlin 委托的理解,包括原理?

码农每日一题 2020-11-22 22:59

kotlin 委托

委托模式是软件设计模式中的一个常用技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。在 java 中实现委托需要我们自己设计代码结构去完成,而 kotlin 直接语法层面支持委托模式,其实现更加优雅、简洁,kotlin 通过关键字 by 来实现委托。

kotlin 类委托

类委托的原理是 by 关键字后面的对象实际会被存储在类的内部,编译器则会将父接口的所有方法实现出来,并且将其转移给委托对象去执行。如下就是一个典型案例:

//定义一个父接口
interface InterfaceBase {
    fun print(i: Int)
}

//定义接口的实现类
class InterfaceBaseImpl (var index: Int): InterfaceBase {
    override fun print(i: Int) {
        println("impl index is $index, params is $i")
    }
}

//类似java方式的kotlin委托实现
class InterfaceBaseDelete(private val base: InterfaceBase): InterfaceBase {
    override fun print(i: Int) {
        base.print(i)
    }
}

//kotlin推荐的类委托实现
//通过 by 关键字就能省略类体达到上面类似java方式的实现
class InterfaceKotlinDelete(private val base: InterfaceBase): InterfaceBase by base

//kotlin推荐的类委托实现的重写
class InterfaceKotlinDelete1(private val base: InterfaceBase): InterfaceBase by base {
    override fun print(i: Int) {
        //优先使用自己的实现
        println("override--params is $i, delete class is ${base.javaClass}")
        //自己实现又调用了委托对象的实现
        base.print(i)
    }
}

/**
 调用结果
 impl index is 2, params is 1
 impl index is 2, params is 4
 impl index is 2, params is 5
 override--params is 6, delete class is class cn.yan.test.InterfaceBaseImpl
 impl index is 2, params is 6
 */

fun testRun() {
    val base = InterfaceBaseImpl(2)
    base.print(1)

    val base1 = InterfaceBaseDelete(base)
    base1.print(4)

    val base2 = InterfaceKotlinDelete(base)
    base2.print(5)

    val base3 = InterfaceKotlinDelete1(base)
    base3.print(6)
}

为了弄清 kotlin 委托类的实现原理,下面看下反编译结果:

yandeMacBook-Pro:test yan$ javap -c InterfaceKotlinDelete.class
Compiled from "Test2.kt"
//InterfaceKotlinDelete实现了InterfaceBase接口
public final class cn.yan.test.InterfaceKotlinDelete implements cn.yan.test.InterfaceBase 
{
  //构造方法接收一个 InterfaceBase 接口类型参数
  public cn.yan.test.InterfaceKotlinDelete(cn.yan.test.InterfaceBase);
    Code:
       0: aload_1
       1: ldc           #12                 // String base
       3: invokestatic  #18                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_0
       7: invokespecial #21                 // Method java/lang/Object."<init>":()V
      10: aload_0
      11: aload_1
      12: putfield      #23                 // Field base:Lcn/yan/test/InterfaceBase;
      15return
  //编译器帮忙实现了委托对象的方法,并且调用了构造函数参数传递对象的对应方法
  public void print(int);
    Code:
       0: aload_0
       1: getfield      #23                 // Field base:Lcn/yan/test/InterfaceBase;
       4: iload_1
       5: invokeinterface #29,  2           // InterfaceMethod cn/yan/test/InterfaceBase.print:(I)V
      10return
}

kotlin 属性委托

对于 kotlin 的属性委托来说,我们有如下要求:

  • 对于只读属性来说(val 修饰的属性),委托需要提供一个名为 getValue 的方法,该方法需要提供的参数如下:

    • thisRef:需要是属性拥有者相同的类型或者是其父类型(对于扩展属性来说,这个类型指的是被扩展的那个类型)。
    • property:需要是KProperty<*>类型或者是其父类型。getValue 方法需要返回与属性相同的类型或者其子类型。
  • 对于可变读写属性(var 修饰的属性),委托需要提供只读属性的 getValue 方法外,还需要提供一个名为 setValue 的方法,该方法需要提供的参数如下:

    • thisRef:需要是属性拥有者相同的类型或者是其父类型(对于扩展属性来说,这个类型指的是被扩展的那个类型)。
    • property:需要是KProperty<*>类型或者是其父类型。
    • value:需要与属性的类型相同或是其父类型。
  • getValue、setValue 方法既可以作为委托类的成员方法实现,也可以作为其扩展方法来实现。

  • getValue、setValue 方法都必须要标记为 operator 关键字。对于委托类来说,它可以实现 ReadOnlyProperty 或是 ReadWriteProperty 接口,这些接口包含了相应的 getValue、setValue 方法。对于委托类来说,它也可以不实现上面两个接口,而单独提供符合约定的 getValue、setValue 方法,其效果是一样的。

我们看一个属性委托样例:

//属性委托方法定义有严格的格式要求
//两个方法的定义签名必须按照要求来,不能修改
class PropertyDelete {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, your deleted property name is ${property.name}"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef, new value is $value")
    }
}

class PropertyClass {
    //通过属性委托,不用给 name 赋值,因为其 set 和 get 方法都被委托到了 PropertyDelete 对象
    var name: String by PropertyDelete()
}

/**
 调用结果
 cn.yan.test.PropertyClass@30946e09, new value is 7890
 cn.yan.test.PropertyClass@30946e09, your deleted property name is name
 */

fun testRun() {
    val test = PropertyClass()
    test.name = "7890"
    println(test.name)
}

属性委托有四种情况在实际开发中比较常用:

  • 延迟属性。
  • 非空属性。
  • 可观测属性。
  • map 委托。

延迟属性: 属性只有在第一次访问时才会计算,之后则会将之前的计算结果缓存起来供后续调用。如下是一个案例:

//延迟属性:依赖 kotlin 提供的 lazy 函数实现,函数参数是一个 lambada 表达式
//源码 LazyJVM.kt 中 public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) 方法
val lazyValue: Int by lazy {
    println("lazyValue lazy")
    28
}

/**
 调用结果
 lazyValue lazy
 28
 28
 */

fun testRun() {
    //首次调用时触发计算
    println(lazyValue)
    //可以看到后续调用是直接用了上次的缓存结果
    println(lazyValue)
}

非空属性: 适用于那些无法在初始化阶段确认属性值的场合。lateinit 修饰符只能在类(不在主构造函数中)内声明的var 属性上使用,而且只有在该属性没有自定义集合或者设置器时,此外属性的类型必须是非空的,并且不能是基元类型。而非空属性没有这些限制。其他他们的作用是相同的。如下是一个案例:

class Tree {
    //非空属性解决了 var name: String? = null 导致后续判断冗余
    //非空属性解决了 var name: String = "" 初值隐晦问题
    //非空属性解决了 lateinit 的一些缺陷,譬如 lateinit 只能应用于非基元类型,譬如不能用于 Int 等问题
    var name: String by Delegates.notNull<String>()
}

/**
 调用
 */

fun testRun() {
    val tree = Tree()
    //运行时异常,没有赋值而使用 IllegalStateException: Property name should be initialized before get.
    //println(tree.name)
    tree.name = "123"
    println(tree.name)
}

可观测属性: kotlin 提供了 observable 赋值后观测器和 vetoable 赋值前拦截观测器的能力。如下是一个案例:

class Tree {
    //可观测属性初值,10 是属性初值,当属性被赋值后会触发回调 lambada 表达式
    var age: Int by Delegates.observable(10) {
        property, oldValue, newValue ->
        println("property name is ${property.name}, old value is $oldValue, new value is $newValue")
    }

    //可观测属性初值,10 是属性初值,当属性被赋值前会触发回调 lambada 表达式,可以做到类似属性值赋值拦截器的效果
    //这里当赋值小于等于0则丢弃
    var defaultCount: Int by Delegates.vetoable(10) {
        property, oldValue, newValue -> when {
            newValue <= 0 -> {
                println("unsupport value of $newValue, refused!")
                return@vetoable false
            }
            else -> {
                println("property name is ${property.name}, old value is $oldValue, new value is $newValue")
                return@vetoable true
            }
        }
    }
}

/**
 调用结果
 property name is age, old value is 10, new value is 10
 property name is age, old value is 10, new value is 11
 property name is age, old value is 11, new value is 12
 12
 -------------
 property name is defaultCount, old value is 10, new value is 100
 unsupport value of -2, refused!
 property name is defaultCount, old value is 100, new value is 101
 101
 */

fun testRun() {
    val tree = Tree()
    tree.age = 10
    tree.age = 11
    tree.age = 12
    println(tree.age)
    println("-------------")
    tree.defaultCount = 100
    tree.defaultCount = -2
    tree.defaultCount = 101
    println(tree.defaultCount)
}

map 委托: 可以将属性值存储到 map 当中。通常出现在 json 解析或者一些动态行为,在这种情况中可以将 map 实例作为类中属性的委托。注意:map 中的 key 名字必须要和属性的名字一致才行,否则委托后运行解析时会抛出 NoSuchElementException 异常提示 key name 不存在。下面是只读 map 属性委托的案例:

class Result (map: Map<String, Any?>) {
    val name: String by map
    val address: String by map
    val age: Int by map
    val date: Date by map

    override fun toString(): String {
        return "{name: ${this.name}, address: ${this.address}, " +
                "age: ${this.age}, date: ${this.date}}"
    }
}

/**
 调用结果
 {name: ruoshui, address: zhuhai, age: 18, date: Sun Oct 18 14:50:21 CST 2019}
 */

fun testRun() {
    val result = Result(mapOf(
        "name" to "ruoshui",
        "address" to "zhuhai",
        "age" to 18,
        "date" to Date()
    ))
    println(result.toString())
}

下面是读写 map 属性委托的案例(可以看到背后都是交给 map 存储的,写属性后 map 会跟着变化):

class Result (map: MutableMap<String, Any?>) {
    var name: String by map
    var address: String by map
    var age: Int by map
    var date: Date by map

    override fun toString(): String {
        return "{name: ${this.name}, address: ${this.address}, " +
                "age: ${this.age}, date: ${this.date}}"
    }
}

/**
 调用结果
 {name: ruoshui, address: zhuhai, age: 18, date: Sun Oct 18 14:57:25 CST 2020}
 ruoshui
 --------
 {name: gongjiang, address: zhuhai, age: 18, date: Sun Oct 18 14:57:25 CST 2020}
 gongjiang
  */

fun testRun() {
    val map: MutableMap<String, Any?> = mutableMapOf(
        "name" to "ruoshui",
        "address" to "zhuhai",
        "age" to 18,
        "date" to Date()
    )
    val result = Result(map)
    println(result.toString())
    println(map["name"])
    println("--------")
    result.name = "gongjiang"
    println(result.toString())
    println(map["name"])
}

对于每个委托属性来说,kotlin 编译器在底层会生成一个辅助的属性,然后将原有属性的访问委托给这个辅助属性。比如说,对于属性 prop 来说,kotlin 编译器所生成的隐含属性为prop$delete,然后对原有的 prop 属性的访问器的访问都只是委托给了这个额外的辅助属性。

提供委托(providing a delegate)

通过定义 provideDelegate operator,我们可以扩展委托的创建过程逻辑。如果对象定义了 provideDelegate 方法,那么该方法就会被调用来创建属性的委托实例。下面是一个案例:

//委托实现类
class PropertyDeleteReadOnlyProperty<AnimBase, String{
    override fun getValue(thisRef: AnimBase, property: KProperty<*>): String {
        return "$thisRef, your deleted property name is ${property.name}"
    }
}
//提供委托实现:作用是当满足特定条件才给属性添加委托
class AnimBaseLauncher {
    operator fun provideDelegate(thisRef: AnimBase, property: KProperty<*>): ReadOnlyProperty<AnimBase, String> {
        println("AnimBaseLauncher provideDelegate invoke...")
        //这里可以类似工厂依据不同条件生产不同的委托属性或者处理逻辑
        when (property.name) {
            "name""address" -> return PropertyDelete()
            else -> throw Exception("property name not valid!")
        }
    }
}

//普通类
class AnimBase {
    val name: String by AnimBaseLauncher()
    val address: String by AnimBaseLauncher()
    //打印输出第二个 AnimBaseLauncher provideDelegate invoke... 后报错 property name not valid!
    //val work: String by AnimBaseLauncher()
}

/**
 调用结果:
 AnimBaseLauncher provideDelegate invoke...
 AnimBaseLauncher provideDelegate invoke...
 cn.yan.test.AnimBase@6e5e91e4, your deleted property name is name
 cn.yan.test.AnimBase@6e5e91e4, your deleted property name is address
  */

fun testRun() {
    val anim = AnimBase()
    println(anim.name)
    println(anim.address)
}

这玩意用的不多,我们知道有这个东西就行,一般自己要是写框架啥的可能会有机会用到。

<扫一扫加开发仔小编 朋友圈技术更精彩>


往期精彩回顾
问:谈谈 Kotlin 范型与逆变协变?
听说你精通 kotlin 函数与 lambda 表达式?

点击左下角阅读原文查看历史经典技术问题汇总,看完顺手一键三连呀~