搞懂Kotlin委托

646 阅读13分钟

1、委托是什么

委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。

委托模式中,有三个角色,约束委托对象被委托对象

委托模式其实不难理解,生活中有很多类似的地方。假如你手里有一套房子想要租出去,想要把房子租出去,联系房客、带人看房是必不可少的,如果让你自己来进行前面所说的工作,可能会占用你自己的业余生活时间,所以这种时候就可以把这些事委托给中介处理,接下来你不需要自己联系房客,也不需要亲自带人看房子,这些工作都由中介完成了,这其实就是一种委托模式。在这里,约束就是联系房客、看房子等一系列操作逻辑,委托对象是你,也就是房主,被委托对象是中介。

在Kotlin中将委托功能分为两种:类委托属性委托

1.1、类委托

类委托的核心思想是将一个类的具体实现委托给另一个类去完成

以上面租房子的例子,我们使用Kotlin的by关键字亲自实现一下委托模式。

首先,我们来定义一下约束类,定义租房子需要的业务:联系房客、看房子。

// 约束类
interface IRentHouse {
    // 联系房客
    fun contact()
    // 带人看房
    fun showHouse()
}

接着,我们来定义被委托类,也就是中介。

// 被委托类,中介
class HouseAgent(private val name: String): IRentHouse {
    override fun contact() {
        println("$name中介 联系房客")
    }
    override fun showHouse() {
        println("$name中介 带人看房")
    }
}

这里我们定义了一个被委托对象,它实现了约束类的接口。

最后,定义委托类,也就是房主。

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse by agent {
    // 签合同
    fun sign() {
        println("房主签合同")
    }
    // 带人看房
    override fun showHouse() {
        println("房主带人看房")
    }
}

这里定义了一个委托类HouseOwner,同时把被委托对象作为委托对象的属性,通过构造方法传入。

在Kotlin中,委托用关键字by,by后面就是委托的对象,可以是一个表达式。

测试:

fun main() {
    val agent = HouseAgent("张三") // 初始化一个名叫张三的中介
    val owner = HouseOwner(agent) // 初始化房主,并把“中介”介绍给他
    owner.contact()
    owner.showHouse()
    owner.sign()
}

运行结果如下:

张三中介 联系房客
房东带人看房
房主签合同

可以看到,在整个租房过程中,房主的一些工作由中介帮着完成,例如联系房客。如果房东心血来潮,觉得自己更理解自己的房子,想把原来委托给中介的工作自己处理,也可以自己来进行,例如带人看房。最后签合同只有房主能处理,所以这是房主独有的操作。以上就是一个委托的简单应用。

而这也是委托模式的意义所在,就是让大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法,那么房东租房的整个逻辑就能顺利进行了。

有的人可能会说,这样的话不用by关键字我也可以实现委托。如下:

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse {
    // 签合同
    fun sign() {
        println("房主签合同")
    }
    // 联系房客
    override fun contact() {
        agent.contact()
    }
    // 带人看房
    override fun showHouse() {
        println("房主带人看房")
    }
}

运行结果如下

张三中介 联系房客
房东带人看房
房主签合同

可以看到,与前面的输出结果一样。

但是这种写法是有一定弊端的,如果约束接口中的待实现方法比较少还好,如果有几十甚至上百个方法的话就会出现问题。

前面也说过委托模式的最大意义在于,大部分的委托类方法实现可以调用被委托对象中的方法。而既然使用委托模式,就说明委托类中的大部分的方法实现是可以通过调用被委托对象中的方法实现的,这样的话每个都像“联系房客”那样去调用被委托对象中的相应方法实现,还不知道要写到猴年马月,而且会产生大量样板代码,很不优雅。

所以Kotlin提供了关键字by,在接口声明的后面使用by关键字,再接上被委托的辅助对象,这样可以免去仅调用被委托对象方法的模版代码。

而如果要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利。

如果想加入独有的方法逻辑,直接写一个方法即可。

这几种情况在前面的“租房”场景中都有体现。

1.2、属性委托

属性委托的核心思想是将一个属性的具体实现委托给另一个类去完成

我们来看一下委托属性的语法结构

class Test {
    // 属性委托
    var prop: Any by Delegate()
}

可以看到,这里使用by关键字连接了左边的prop属性和右边的Delegate实例,这种写法就代表着将prop属性的具体实现委托给了Delegate类去完成

1.2.1、什么是属性委托

前面也说了属性委托是将一个属性的具体实现委托给另一个类去完成。那么属性把什么委托了出去,被委托类又有哪些实现呢?

其实,属性委托出去的是其set/get方法,委托给了被委托类的setValue/getValue方法。

// 属性的get/set方法
var prop: Any
    get() {}
    set(value) {}

// 委托后
var prop: Any by Delegate()

注意这里prop声明的是var,即可变变量,如果委托给Delegate类的话,则必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

class Delegate {
    private var propValue: String? = null

    operator fun getValue(thisRef: Any, property: KProperty<*>): String? {
        return propValue
    }

    operator fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
        propValue = value
    }
}

到这里,属性委托已经完成了,这时候,当你点开by关键字的时候会出现如下提示。

这就表明prop已经把具体实现委托给Delegate类完成。

当调用prop属性的时候会自动调用Delegate类的getValue()方法,当给prop属性赋值的时候会自动调用Delegate类的setValue()方法。

如果prop声明的是val,即不可变变量,则Delegate类只需要实现getValue()方法即可。

有些人第一次看到方法中的参数可能有点懵,但其实这是一种标准的代码实现样板,并且官方也提供了接口类帮助我们实现,具体的接口类下面会说到。

虽然是一套固定的样板,但我们也要理解其中参数的含义。

  • thisRef:用于声明该Delegate类的委托功能可以在什么类中使用。必须与 属性所在类 类型相同或者是它的父类,如果是扩展函数,则指的是扩展的类型。
  • property:KProperty是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。多数情况下都不需要修改。
  • value:具体要赋值给委托属性的值,必须与getValue的返回值相同。

那么为什么要使用属性委托呢?

假如想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段field中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作委托给一个辅助对象(类),这个辅助对象就是被委托类。说白了,就是避免样板代码,防止出现大量重复逻辑。

1.2.2、ReadOnlyProperty/ReadWriteProperty接口

前面说到,如果要实现属性委托,就必须要实现getValue/setValue方法,可以看到getValue/setValue方法结构比较复杂,很容易遗忘,为了解决这个问题,Kotlin 标准库中声明了2个含所需operator方法的 ReadOnlyProperty/ReadWriteProperty 接口。

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

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

val属性实现ReadOnlyProperty接口,var属性实现ReadWriteProperty接口。这样就可以避免自己写复杂的实现方法了。

// val 属性委托实现
class Delegate1: ReadOnlyProperty<Any,String>{
    private var propValue: String = "zkl"
    
    override fun getValue(thisRef: Any, property: KProperty<*>): String {
        return propValue
    }
}
// var 属性委托实现
class Delegate2: ReadWriteProperty<Any,String?>{
    private var propValue: String? = null
    
    override fun getValue(thisRef: Any, property: KProperty<*>): String? {
        return  propValue
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
       propValue = value
    }
}

2、Kotlin标准库的几种委托

2.1、延迟属性 lazy

2.1.1、使用by lazy进行延迟初始化

使用by lazy()进行延迟初始化相信大家都不陌生,在日常使用中也能信手拈来。如下,是DataStoreManager对象延迟初始化的例子。

//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    DataStoreManager(store) }

by lazy()代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当变量(instance)首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋值给变量。 调用如下:

fun main() {
    println(instance::class.java.simpleName)
    println(instance::class.java.simpleName)
    println(instance::class.java.simpleName)
}

打印结果如下:

第一次调用时执行
DataStoreManager
DataStoreManager
DataStoreManager

可以看到,只有第一次调用才会执行代码块中的逻辑,后续调用只会返回代码块的最终值

那么什么时候适合使用by lazy进行延迟初始化呢?当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,就非常适合了。

当然,如果变量第一次初始化时抛出异常,那么lazy将尝试在下次访问时重新初始化该变量。

2.1.2、拆解by lazy

可能大家刚接触的时候会觉得by lazy本是一体使用的,其实不是,实际上,by lazy并不是连在一起的关键词,只有by才是Kotlin中的关键字,lazy只是一个标准库函数而已

那么就把二者拆开看,先点开by关键字

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

会发现它是Lazy 类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy类中,这个value便是返回的值。

//惰性初始化类
public interface Lazy<out T> {
    
    //懒加载的值,一旦被赋值,将不会被改变
    public val value: T

    //表示是否已经初始化
    public fun isInitialized(): Boolean
}

接下来看一下lazy,这个就是一个高阶函数,用来创建lazy实例的。

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

可以看到,该方法中会把initializer,也就是代码块中的内容,传递给SynchronizedLazyImpl类进行初始化并返回。大部分情况我们使用的都是这个方法。

当然我们也可以设置mode,这样会调用下面的lazy方法,该方法中会根据mode类型来判断初始化那个类。如下

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

// 使用如下
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    println("第一次调用时执行")
    DataStoreManager(store) 
}

三个mode解释如下:

  • LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

而第一个lazy不设置mode时默认的就是SYNCHRONIZED,也是最常用的mode,这里我们直接看一下对应类的代码:

//线程安全模式下的单例
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    //用来保存值,当已经被初始化时则不是默认值
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    //锁
    private val lock = lock ?: this

    override val value: T
        //见分析1
        get() {
            //第一次判空,当实例存在则直接返回
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            //使用锁进行同步
            return synchronized(lock) {
                //第二次判空
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    //真正初始化
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    //是否已经完成
    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

这个单例就是双重校验锁实现的。

2.2、可观察属性

Kotlin除了提供了lazy函数实现属性延迟加载外,还提供了Delegates.observableDelegates.vetoable标准库函数来观察属性变化。先来看observable

2.2.1、observable函数
    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)
        }

可以看到,该标准库函数接收了两个参数initialValueonChange

  • initialValue:初始值
  • onChange:属性值变化时的回调逻辑。回调有三个参数:propertyoldValuenewValue,分别表示:属性、旧值、新值。

使用如下:

var observableProp: String by Delegates.observable("初始值") { property, oldValue, newValue ->
    println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
}
// 测试
fun main() {
    observableProp = "第一次修改值"
    observableProp = "第二次修改值"
}

打印如下:

属性:observableProp 旧值:初始值 新值:第一次修改值 
属性:observableProp 旧值:第一次修改值 新值:第二次修改值 

可以看到,当把属性委托给Delegates.observable后,每一次赋值,都能观察到属性的变化。

2.2.2、vetoable函数

vetoable函数与observable一样,都可以观察属性值变化,不同的是,vetoable可以通过代码块逻辑决定属性值是否生效。

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

接收的两个参数与observable函数几乎相同,不同的是onChange回调有一个Boolean的返回值。

使用如下:

var vetoableProp: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
    println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
    newValue > 0
}
// 测试
fun main() {
    println("vetoableProp:$vetoableProp")
    vetoableProp = 2
    println("vetoableProp:$vetoableProp")
    vetoableProp = -1
    println("vetoableProp:$vetoableProp")
    vetoableProp = 3
    println("vetoableProp:$vetoableProp")
}

打印如下:

vetoableProp:0
属性:vetoableProp 旧值:0 新值:2
vetoableProp:2
属性:vetoableProp 旧值:2 新值:-1
vetoableProp:2
属性:vetoableProp 旧值:2 新值:3
vetoableProp:3

可以看到-1的赋值并没有生效。

那么具体的逻辑是什么呢?

回看observable和vetoable的源码可以发现,二者继承了ObservableProperty抽象类,不同的是observable重写了该类afterChange方法,vetoable重写了该类beforeChange方法,并且beforeChange会有一个Boolean的返回,返回的是我们自己写的回调逻辑的返回值。

那么接着看setValue逻辑

protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true

protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {}

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
        val oldValue = this.value
        if (!beforeChange(property, oldValue, value)) {
            return
        }
        this.value = value
        afterChange(property, oldValue, value)
    }

可以看到会先执行beforeChange方法,如果beforeChange为false则直接返回,并且不会更新值,为true时才会更新值,接着执行afterChange方法。这里beforeChange方法默认返回true。

其实只要查看这些函数源码可以发现,其内部调用的都是代理类,所以说白了这些都是属性委托。

3、总结

委托在Kotlin中有着至关重要的作用,但是也不能滥用,中间毕竟多了一个中间类,不合理的使用不但不会有帮助,反而会占用内存。前面也说过类委托最大的意义在于,大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法。属性委托的意义在于,对于比较复杂的一些属性,它们处理起来比把值保存在支持字段field中更复杂,且它们的逻辑相同,为了防止出现大量模版代码,可以使用属性委托。所以在使用委托前,我们也可以按照上面的标准考虑一下,合理的使用委托可以减少大量样板代码,提高代码的可扩展性和可读性。