Kotlin-委托

136 阅读7分钟

一、关于委托

委托在实际开发中一直不受重视,这是因为委托的应用场景不清晰,但Kotlin 的委托在软件架构中可以发挥巨大的作用,例如Jetpack Compose中就大量使用了委托。

Kotlin的委托主要是二个应用场景,一个是委托类,另一个是委托属性,接下来一个个分析。

二、委托类

定义一个Animal接口,并增加一个需要实现的方法eat,如下:

interface Animal {
    fun eat()
}

然后增加二个实现类,兔子和老虎,如下:

class Rabbit : Animal {
    override fun eat() {
        print("吃草")
    }
}
class Tiger : Animal {
    override fun eat() {
        print("吃肉")
    }
}

假如现在要增加狮子,然后狮子也吃肉,跟老虎的eat一模一样,那假如我们增加一个狮子类,如下:

class Lion:Animal {
    override fun eat() {
        print("吃肉")
    }
}

就会发现除了类名不一样,狮子Lioneat行为和老虎Tigereat行为一样,我们多写了很多模板代码,于是我们就可以将狮子Lioneat行为委托给老虎Tiger。在kotlin中用by关键字就可以实现委托,如下:

class Lion(tiger: Tiger) : Animal by tiger

//使用
Lion(Tiger()).eat()

可以看到虽然我们实现了Animal接口,但是我们并没有写任何实现,这就是委托,通过by关键字将实现全部委托给了传入的tiger:Tiger。如果将传入的对象改为接口animal:Animal而不是接口的子类则更具扩展性(通用):

class Lion(animal: Animal) : Animal by animal

所以说,Kotlin 的委托类提供了语法层面的委托模式。通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。

三、委托属性

Kotlin“委托类”委托的是接口方法,而“委托属性”委托的,则是属性的getter、setter。属性的 getter、setter 委托出去以后,能有什么用呢?我们可以从 Kotlin 官方提供的标准委托那里找到答案。

1、标准委托

Kotlin 提供了好几种标准委托,其中包括两个属性之间的直接委托by lazy 懒加载委托Delegates.observable 观察者委托,以及 by map 映射委托。前面两个的使用频率比较高,后面两个频率比较低。这里,我们就主要来了解下前两种委托属性。

①将属性 A 委托给属性 B

从 Kotlin 1.4 开始,我们可以直接在语法层面将“属性 A”委托给“属性 B”,就像下面这样:

class Person {
    var age: Int = 18
    //将newAge的getter、setter委托给age
    //::age是属性的引用
    var newAge: Int by ::age
}

agenewAge 两者之间的委托关系一旦建立,就代表了它们两者的 getter 和 setter 会完全绑定在一起,如果要用代码来解释它们背后的逻辑,它们之间的关系会是这样:

class Person {
    var age: Int = 18
    
    var newAge: Int
        get() = age
        set(value) {
            age = value
        }
}

这种委托的应用场景是可以做到软件版本的兼容,比如2.0版本之前用的age字段,而2.0版本之后用newAge字段,我们可以将newAge委托给age就能保证不同的软件版本取到的不管是age还是newAge取到的值是一致的。

②懒加载委托

被访问的时候才去触发,从而避免不必要的资源开销。例如创建集合:

val list: MutableList<Int> by lazy { mutableListOf() }

懒加载委托的源代码,其实是一个高阶函数:

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

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

可以看到,lazy() 函数可以接收一个LazyThreadSafetyMode 类型的参数,如果我们不传这个参数,它就会直接使用SynchronizedLazyImpl的方式。而且通过它的名字我们也能猜出来,它是为了多线程同步的。而剩下的 SafePublicationLazyImplUnsafeLazyImpl,则不是多线程安全的。

③Delegates.observable 观察者委托

看如下的代码:

class Person {
    var age: Int by Delegates.observable(0) { property, oldValue, newValue -> print("observable:oldValue:$oldValue,newValue:$newValue") }
}

0为初始值,调用代码修改这个初始值:

val person = Person()
person.age = 15
person.age = 18

可以发现打印了二条print,这个属性的代理比较简单,当属性改变时会发出通知。除了这个还有另外一个,当返回值为true时允许修改属性值,为false则返回原值,如下:

class Person {
     var age: Int by Delegates.vetoable(0) { property, oldValue, newValue -> getResult(property,oldValue,newValue) }

    private fun getResult(property: KProperty<*>, oldValue: Int, newValue: Int): Boolean {
        Log.d("", "vetoable:oldValue:$oldValue,newValue:$newValue")
        return true
    }
}

④by map 映射委托

在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。看如下的代码:

class Person(var map: MutableMap<String, Any?>) {
    var userName: String by map
    var age: Int by map
    var nickName: String by map
    var isAult: Boolean by map
}

调用,委托属性会从这个映射中取值(通过字符串键——属性的名称):

val person = Person(mutableMapOf("userName" to "张三", "age" to 18,"nickName" to "大牛","isAult" to false))
Log.d("TAG", "person:${person.userName},${person.age},${person.nickName},${person.isAult}") //person:张三,18,大牛,false

2、自定义委托属性

①自定义实现

自定义委托,必须遵循 Kotlin 制定的规则。如下代码:

class StringDelegate(private var oldValue: String = "Hello") {

    operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return oldValue
    }

    operator fun setValue(thisRef: Owner, property: KProperty<*>, newValue: String) {
        oldValue = newValue
    }
}

class Owner {
    var text: String by StringDelegate("World")

    init {
        Log.d("TAG","text:$text")
    }
}

1)、两个方法必须有 operator 关键字修饰
2)、text 属性是处于 Owner 这个类当中的,因此getValuesetValue 这两个方法中的 thisRef 的类型,必须要是 Owner,或者是 Owner 的父类。
3)、text 属性是 String 类型的,为了实现对它的委托,getValue 的返回值类型,以及 setValue 的参数类型,都必须是 String 类型或者是它的父类。

②ReadWriteProperty、ReadOnlyProperty

借助 Kotlin 提供的 ReadWriteProperty、ReadOnlyProperty 这两个接口,来自定义委托。源码如下:

package kotlin.properties

import kotlin.reflect.KProperty

/**
 * Base interface that can be used for implementing property delegates of read-only properties.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your property delegate has methods with the same signatures.
 *
 * @param T the type of object which owns the delegated property.
 * @param V the type of the property value.
 */
public fun interface ReadOnlyProperty<in T, out V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

/**
 * Base interface that can be used for implementing property delegates of read-write properties.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your property delegate has methods with the same signatures.
 *
 * @param T the type of object which owns the delegated property.
 * @param V the type of the property value.
 */
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V

    /**
     * Sets the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @param value the value to set.
     */
    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

/**
 * Base interface that can be used for implementing property delegate providers.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your delegate provider has a method with the same signature.
 *
 * @param T the type of object which owns the delegated property.
 * @param D the type of property delegates this provider provides.
 */
@SinceKotlin("1.4")
public fun interface PropertyDelegateProvider<in T, out D> {
    /**
     * Returns the delegate of the property for the given object.
     *
     * This function can be used to extend the logic of creating the object (e.g. perform validation checks)
     * to which the property implementation is delegated.
     *
     * @param thisRef the object for which property delegate is requested.
     * @param property the metadata for the property.
     * @return the property delegate.
     */
    public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
}

如果我们需要为 val 属性定义委托,我们就去实现 ReadOnlyProperty这个接口;如果我们需要为 var 属性定义委托,我们就去实现 ReadWriteProperty 这个接口。这样做的好处是,通过实现接口的方式,IntelliJ 可以帮我们自动生成 overridegetValuesetValue 方法。以前面的代码为例,我们的 StringDelegate,也可以通过实现 ReadWriteProperty 接口来编写:

class StringDelegate(private var oldValue: String = "Hello") : ReadWriteProperty<Owner, String> {

    override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return oldValue
    }

    override operator fun setValue(thisRef: Owner, property: KProperty<*>, newValue: String) {
        oldValue = newValue
    }
}

class Owner {
    var text: String by StringDelegate("World")

    init {
        Log.d("TAG", "text:$text")
    }
}

③提供委托(provideDelegate)

在前面二点的基础上,假如我们想在委托属性之前做点事情,我们可以使用 provideDelegate 来实现(看上面的Android源码)。看如下代码:

class StringDelegate(private var oldValue: String = "Hello") : ReadWriteProperty<Owner, String> {

    override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return oldValue
    }

    override operator fun setValue(thisRef: Owner, property: KProperty<*>, newValue: String) {
        oldValue = newValue
    }
}

class SmartDelegator : PropertyDelegateProvider<Owner, StringDelegate> {
    override fun provideDelegate(thisRef: Owner, property: KProperty<*>): StringDelegate {
        return if (property.name == "log") {
            StringDelegate("Log")
        } else {
            StringDelegate("Print")
        }
    }
}

class Owner {
    var log: String by SmartDelegator()
    var print: String by SmartDelegator()

    init {
        Log.d("TAG", "value:log:$log,print:$print")
    }
}

//打印结果
value:log:Log,print:Print

通过 provideDelegate 这样的方式,我们不仅可以嵌套 Delegator,还可以根据不同的逻辑派发不同的Delegator

四、委托实战案例

1、属性可见性封装

在java中我们防止外部修改整个集合的值是很困难的,但在kotlin中借助代理就很简单,如下:

class Model {
    val data: List<String> by ::_data
    private val _data: MutableList<String> = mutableListOf()

    fun addValue() {
        _data.add("hello")
        _data.add("world")
    }
}

data的集合类型为List,没有addremove修改的方法。_data是私有的不对外暴露。_data修改的方法只有自己可以通过addValue调用。

2、数据与 View 的绑定

在 Android 当中,如果我们要对“数据”与View进行绑定,我们可以用 DataBinding,不过 DataBinding 太重了,也会影响编译速度。其实,除了 DataBinding 以外,我们还可以借助 Kotlin 的自定义委托属性来实现类似的功能。这种方式不一定完美,但也是一个有趣的思路。我们让TextView也提供代理,代码如下:

operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) =
    object : ReadWriteProperty<Any?, String?> {
        override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text?.toString()
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
            text = value
        }
    }

使用

val textView = findViewById<TextView>(R.id.tv)

var msg: String? by textView

textView.text = "hello"
Log.d("TAG", "msg:$msg")  //msg:hello

msg = "world"
Log.d("TAG", "textView.text:${textView.text}") //textView.text:world

这个例子主要体现在思路,可以按自己的想法为对象(TextView)提供代理。

3、ViewModel 委托

在 Android 当中,我们会经常用到 ViewModel 来存储界面数据。同时,我们不会直接创建 ViewModel 的实例,而对应的,我们会使用委托的方式来实现。代码如下:

// MainActivity.kt
private val mainViewModel: MainViewModel by viewModels()

看下ViewModel的委托是如何实现的

@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }

    return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

原来viewModelsComponentActivity的扩展函数,这里不讨论ViewModel是如何创建出来的,不是本节的讨论范畴,我们继续看ViewModelLazy方法,源码如下:

public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory,
    private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {   //实现了Lazy接口
    private var cached: VM? = null   //缓存(是否创建过)

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {  //没有创建过
                val factory = factoryProducer()
                val store = storeProducer()
                //创建了ViewModel
                ViewModelProvider(
                    store,
                    factory,
                    extrasProducer()
                ).get(viewModelClass.java).also {
                    cached = it
                }
            } else {  //创建过
                viewModel
            }
        }

    //是否被初始化过(没有被初始化过不能toString)
    override fun isInitialized(): Boolean = cached != null
}

具体看代码里面的注释。

4、自己实现委托

我们模仿by viewModels实现自己的委托。首先是接口和子类:

interface Animal {
    fun eat()
}
class Rabbit : Animal {

    override fun eat() {
        print("吃草")
    }

    fun getRabbit(): String = "Hello Rabbit"
}

具体的代理实现代码:

inline fun <reified AN : Animal> ComponentActivity.animals(): Lazy<AN> {
    return AnimalLazy(AN::class)
}

class AnimalLazy<AN : Animal> constructor(private val animalClass: KClass<AN>) : Lazy<AN> {
    private var cached: AN? = null

    override val value: AN
        get() {
            return cached ?: animalClass.java.newInstance()
        }

    override fun isInitialized(): Boolean = cached != null
}

调用

private val rabbit: Rabbit by animals()

Log.d("TAG", rabbit.getRabbit())  //Hello Rabbit

个人学习笔记

参考了以下内容

委托属性

委托:你为何总是被低估?