一文理解 Kotlin 的委托

Kotlin 的委托分为两种,一种是类委托,一种是属性委托,还有一个特殊的委托提供者(委托的委托)。下面会依次进行介绍。

类委托

类委托示例如下:

kotlin 复制代码
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

它其实等价于:

kotlin 复制代码
class Derived(val b: Base): Base {
    override fun print() {
        b.print()
    }
}

可以看到类委托其实就是委托模式的语法糖,通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。

属性委托

属性委托示例如下。其中 Delegate 类的 setValue 和 getValue 方法是固定的写法,不可修改。当我们委托 var 可变变量时,setValue 和 getValue 方法都需要。当我们委托 val 不可变变量时,只需要 getValue 方法。

kotlin 复制代码
class Example {
    var p: String by Delegate()
}


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

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

它可以近似等价于:

kotlin 复制代码
class Example {
    
    private val p_delegate = Delegate()
    var p: String
        set(value) {
            p_delegate.setValue(this, ::p, value)
        }
        get() {
            return p_delegate.getValue(this, ::p)
        }
    
}

可以看到,属性委托其实是对属性的 set、get 方法的委托 。其中 KProperty 是属性的元数据,例如此处是p 的元数据,使用反射可以获取这些数据。

在开发过程中,setValue 和 getValue 的固定的写法太麻烦了。Google 就提供了 ReadWriteProperty 和 ReadOnlyProperty 两个接口,你只要根据需要实现其中一个接口就可以了,代码示例如下:

kotlin 复制代码
//使用 ReadWriteProperty 委托 var 变量
class Delegate: ReadWriteProperty<Example, String> {
    override fun getValue(thisRef: Example, property: KProperty<*>): String {
        return ""
    }

    override fun setValue(thisRef: Example, property: KProperty<*>, value: String) {
        
    }
}

//使用 ReadOnlyProperty 委托 val 变量
class Delegate: ReadOnlyProperty<Example, String> {
    override fun getValue(thisRef: Example, property: KProperty<*>): String {
        return ""
    }
}

标准委托

大部分情况下,我们不需要自定义委托,而是使用标准委托。标准委托有四种,分别是:委托给另一个属性、懒加载委托、观察委托以及映射委托。

委托给另一个属性

一个属性可以把它的 getter 与 setter 委托给另一个属性。这种委托对于顶层和类的属性(成员和扩展)都可以使用。

kotlin 复制代码
class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // 通知:'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

代码示例如上所示,属性委托给另一个属性的方式在处理兼容情况时非常有用。当你想要以一种向后兼容的方式重命名一个属性的时候,就可以引入一个新的属性、 使用 @Deprecated 注解来注解旧的属性、并委托其实现。

by lazy 懒加载委托

对于一些需要消耗计算机资源的操作,我们希望它在被访问的时候才去触发,从而避免不必要的资源开销,这就是懒加载。在 kotlin 中,我们使用 by lazy 懒加载委托就可以简单的实现懒加载的功能。代码示例如下:

kotlin 复制代码
val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

lazy 的实现如下所示。可以看到,默认情况下 lazy 实现懒加载是线程安全的。其中 PUBLICATION 模式会调用多次初始化方法,但只有第一次的有效;NONE 模式会调用多次,且会改变常量的值为最后一次的值,他不是线程安全的。

kotlin 复制代码
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)
    }

Delegates 观察委托

我们可以使用 Delegates 观察委托,它有两种方法 observable 和 vetoable。observable 会监听属性的变化,vetoable 会拦截赋值,并根据判断条件来决定要不要赋值。

kotlin 复制代码
    // -1 是初始值
    var data by Delegates.observable(-1) { property, oldValue, newValue ->
        //监听 data 的变化
        println("oldValue = $oldValue newValue = $newValue")
    }
    // 0 是初始值
    var size by Delegates.vetoable(0) { property, oldValue, newValue ->
        //设置更新条件,返回为 true 时才会赋新值
        newValue < 100
    }

by map 映射委托

映射委托用的场景比较少。它的功能主要是将 Map 中的 key-value 值委托出去,代码示例如下:

kotlin 复制代码
class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

如果你想要委托 var 变量,这时需要 MutableMap 。

委托提供者(委托的委托)

当委托比较复杂时,可能需要根据需要创建不同的委托。这时候就需要使用委托提供者 ,或者叫做委托的委托。它和 setValue 、 getValue 方法一样有固定的写法。代码如下所示:

kotlin 复制代码
operator fun provideDelegate(thisRef: T, property: KProperty<*>): D

不过这个写法不用记住,它同样有对应的接口 PropertyDelegateProvider,只要实现它就好了。我们知道,在学校老师和学生在食堂吃饭的价格是不一样的,这时如果需要设计一个刷卡系统,就可以用到委托提供者 。示例如下,我们根据智能卡的 id 来判断当前用户的身份是老师还是学生,如果是老师则使用 TeacherConsumeDelegate 执行老师的代理扣费服务;反之则执行 StudentConsumeDelegate 学生的代理扣费服务。

kotlin 复制代码
class CardDelegateProvider: PropertyDelegateProvider<Card, ReadOnlyProperty<Card, IConsumeStrategy>> {
    override fun provideDelegate(
        thisRef: Card,
        property: KProperty<*>
    ): ReadOnlyProperty<Card, IConsumeStrategy> {
        
        val delegate = if(thisRef.id.startsWith("tech_")) {
            TeacherConsumeDelegate()
        } else {
            StudentConsumeDelegate()
        }
        return delegate
    }
}
相关推荐
海水冷却5 小时前
Android 群组聊天怎么实现:基于 ZIM 的完整落地指南(附可运行 Demo)
android
三少爷的鞋8 小时前
现代 Android 架构指南:不要再主动设计 Callback 接口
android
TimeFine14 小时前
AI 编码工作流的设计
android·ai编程
骑着蜗牛撵大象32714 小时前
C++20 协程 × 屏障:手写一个 co_await 版 Barrier,让等待不再钉死线程
android·c++20·协程·并发编程·co_await·屏障同步
山东孙燕姿16 小时前
设备台账重构:从一张表到三层模型
android·java·网络·低代码·重构·rxjava
蒸鱼Yuzheng17 小时前
移动端 CPU 调度分析:线程亲和性、大小核迁移与卡顿
android·perfetto·性能分析·线程优化·cpu调度
booboosui17 小时前
安卓唤醒词:12 维 MFCC 加 DTW,阈值 1.8
android
传奇开心果17 小时前
Compose Multiplatform 共享边界实战:架构契约、互操作同步与可回退设计
android·kotlin
事圆则缓17 小时前
从开机到首帧:Android 系统与应用启动流程源码分析
android
AI吃大瓜18 小时前
指针压力表读数4:Android实现指针压力表读数(可实时检测)
android·指针读数·指针压力表·压力表·压力表读数