kotlin查漏补缺系列(4)——委托

724 阅读5分钟

我正在参加「掘金·启航计划」

前言

本文是kotlin查漏补缺系列的第4篇,关于kotlin委托的分享,耐心看完本文你将学会

  • kotlin的委托是什么?
  • 类委托和属性委托分别是什么?怎么用?

什么是委托

委托也可叫代理,是一种常用的设计模式,具体表现为将对象的某些行为委托给其他的类来实现,打个比方,我想把房东的房子卖了,那我可以委托给房屋中介去帮我销售,这就是一种委托,其中我自己是委托对象,而中介是被委托对象

在kotlin中,委托分为类委托属性委托,通过关键字by来实现

类委托

类委托指的是在类的层面进行委托代理,假设类A类B都实现了同一个接口,接口定义了一个方法methodA,通过这里的类委托就能够把类A委托给类B,当我们调用类A对象的methodA方法时,内部实际会去执行类B中methodA方法的逻辑,有点儿绕?看栗子

还是来看卖房子的例子,我们可以先定义一个ISaleHouse接口接口,里面有一个函数表示卖房子

interface ISaleHouse {
    // 卖房子
    fun sale()
}

定义房屋中介类,实现ISaleHouse接口,这个类将作为被委托类

class DelegateAgent:ISaleHouse {
    override fun sale() {
        println("delegate sale")
    }
}

再定义房屋所有者,同样实现ISaleHouse接口,这个类将作为委托类

class HouseOwner(agent:ISaleHouse):ISaleHouse by agent

注意到这个类的构造方法接收了一个ISaleHouse接口的对象,并且通过by关键字把这个类委托给了传进来的这个对象

这样就已经实现了kotlin的类委托,很简单吧,直接一个by关键字就解决,不用再像java一样,还需要在委托类中手动去调用被委托对象的相应方法

最后来验证一下

fun main(){
    val agent = DelegateAgent()
    val owner = HouseOwner(agent)
    owner.sale()
}

// 运行结果
delegate sale

demo中把HouseOwner的对象Owner委托给了DelegateAgent对象agent,调用owner的sale方法时,运行结果打印的是被委托类DlegateAgent中sale方法的代码,委托完成

属性委托

看了上面的类委托的例子,不难看出委托的本质:把一个对象的某个函数委托给另外一个对象的相应函数去处理

那么属性委托也不例外,其实质也是对函数进行委托,那属性有什么函数?get和set

所以kotlin的属性委托,就是把属性的get和set方法委托给被委托对象,而被委托对象需要提供两个方法,setValuegetValue,对应的get方法将会委托给getValue,set方法委托给setValue,如果属性是val修饰的,只需要有getValue就够了


class DelegateProp{
    operator fun getValue(thisRef:Any?,prop:KProperty<*>):String{
        return "getValue ref is $thisRef,prop is ${prop.name}"
    }

    operator fun setValue(thisRef:Any?,prop:KProperty<*>,value:String){
        println("setValue ref is $thisRef,prop is ${prop.name}")
    }
}

class TestProp{
    var myProp:String by DelegateProp()
}

fun main(){
    val testProp = TestProp()
    testProp.myProp = "1"
    val a = testProp.myProp
}

demo中定义了DelegateProp类,提供了getValuesetValue两个函数,注意这两个函数必须被operator修饰的

然后把属性myProp通过by关键字委托给了DelegateProp对象,这样就实现了属性委托

getValuesetValue两个函数两个函数中,有两个关键参数,thisRefprop

  • thisRef:必须是属性所在类型或者其超类
  • prop:必须是KProperty类型或者其超类

如果每次写都需要像demo中这样一个个手敲多少有点不讲武德,不过kotlin肯定考虑到了,在标准库中给我们提供了两个接口,ReadOnlyPropertyReadWriteProperty,如果属性是val修饰的,那么被委托类只需要实现ReadOnlyProperty接口就可以,如果是var修饰的,则要去实现ReadWriteProperty

public fun interface ReadOnlyProperty<in T, out V> {
  
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}


public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V

    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

具体的使用就不多介绍了,就是实现接口的区别

使用场景

属性委托在kotlin中的使用是很常见的,例如延迟初始化by lazy,可观察属性Delegates.Observable,属性和Map的映射

延迟初始化

不可变的属性,在使用的时候才去初始化,属于一种懒加载

val layProp:String by lazy { 
    "test"
}

简单看看lazy的源码

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

lazy是一个库函数,接收一个lambda表达式,并且返回一个Lazy对象,所以,这个属性就是被委托给了Lazy类型的对象,而且延迟属性都是val修饰的,所以Lazy应该是提供了一个getValue的方法

那我们看看Lazy,发现它只是一个接口,接口里面没有我们想看到的getValue

public interface Lazy<out T> {
   
    public val value: T

    public fun isInitialized(): Boolean
}

但是这个接口有一个扩展函数getValue,所以就是通过这个来实现属性委托的

public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

实现延迟初始化的的原理也很简单,就是在get的时候,去执行lambda里面的内容,这里就不继续展开讲了

可观察属性Delegates.Observable

可观察属性利用了观察者模式,通过把属性委托给Delegates.Observable,能够监测到这个属性的变化,看栗子

var observablePrp by Delegates.observable(1) { property, oldValue, newValue ->
    println("property is $property,old is $oldValue,new is $newValue")
}

Delegates.observable是一个函数,接收两个参数,第一个是初始值,第二个是一个lambda表达式,lambda有三个参数

  • property:被修改的属性
  • oldValue:原始值
  • newValue:修改后的值

当这个属性值被修改后,就会回调到传入的lambda表达式,例如开发中某个属性是一个标记位,需要在标记位被修改之后立马更新到文件中,就可以使用到这个语法在lambda中去处理,非常方便

Delegates.Observable的源码是一个很标准的属性委托,函数直接返回的是一个ReadWriteProperty类型

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

属性和Map的映射

在开发中,可能会遇到这样一种场景,一个实体类跟一个map能够对应上,也就是map中存储了实体对象某些属性的值,这时候,我们可以让类的构造函数去接收一个map对象,然后将属性委托给这个map,这样就不用单独去赋值了

class TestProp(map: Map<String,Any?>){

    val mapPro by map

    val mapPro1 by map

}

fun main(){
    val map = mapOf("mapPro" to 1,"mapPro1" to "str")
    val testProp = TestProp(map)
    println("map pro is ${testProp.mapPro1}")
}

查看源码可以发现,Map提供了getValue的扩展方法,当读取某个属性的值时,会在getValue中拿到这个属性的名字,然后用这个名字作为key,去调用get方法获取对应的值

public inline operator fun <V, V1 : V> Map<in String, @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
    @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1)

当然如果属性是var的,委托的map则也需要是MutableMap类型的,只有MutableMap才提供了setValue的扩展方法

以上就是关于kotlin委托的全部内容,欢迎在评论区交流