Kotlin 委托机制:搞懂“老板与秘书”模式

"委托"是一种非常经典的设计模式,它的基本理念很简单,就是一个对象不自己去处理某个请求,而是将请求转交给另一个辅助对象去处理。

这就好比"老板与秘书"的关系:老板在接到单后,会把活都揽给秘书(辅助对象)去做,自己当甩手掌柜。

Kotlin 原生就支持委托,并将其分为了两类:类委托委托属性

类委托:替代继承的绝佳方案

类委托的核心思想是:将一个类的具体实现委托给另一个对象来完成。

在面向对象编程中,为了降低类之间的耦合性以及减少类层级,我们会推荐使用组合,而非继承。而类委托就是实现"组合"的一种绝妙的方式。

传统写法

如果没有类委托,会发生什么?

例如,我们要实现一个自定义的 Set,可以通过组合一个 HashSet 来实现:

在这个场景中,Set 接口就是"岗位职责",MySetManual 是老板,传入的 HashSet 是实际干活的"秘书"。

kotlin 复制代码
class MySetManual<T>(private val helperSet: HashSet<T>) : Set<T> {
    // 老板需要书写大量样板代码,手动将请求转给秘书 helperSet
    override val size: Int
        get() = helperSet.size

    override fun contains(element: T) = helperSet.contains(element)
    override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
    override fun isEmpty() = helperSet.isEmpty()
    override fun iterator() = helperSet.iterator()
}

可以看到,在所有的方法实现中,都只是简单调用了 helperSet 对象的同名方法,这是一种手动的委托模式。

意义: 通过这种"组合",我们可以很方便地选择性重写部分方法的实现 / 提供新的方法,增强已有对象的能力。

但问题是:要是接口有几十个方法,难道老板需要全部都重写一遍,并手动分配任务吗?

Kotlin 的 by 关键字

Kotlin 为委托提供了 by 关键字。我们只需在接口声明后使用它,再加上被委托的辅助对象即可,所有的派发样板代码都会由编译器自动生成,使代码量大大减少:

kotlin 复制代码
class MyMutableSet<T>(
    private val helperSet: MutableSet<T> = HashSet(),
) : MutableSet<T> by helperSet { // 签订"外包合同",helperSet 会接管 MutableSet 接口的所有工作

    // 老板只负责重写需要定制的方法,增加特殊处理
    override fun isEmpty(): Boolean {
        println("Performing custom isEmpty check!")
        return size == 0
    }

    // 也可以新增方法,扩展功能
    fun getRandomOrNull(): T? {
        return helperSet.randomOrNull()
    }
}

简单来说,使用 Class 接口 by 秘书对象,接口的工作都会交给秘书,自己只负责重写需要定制的方法。

属性委托:接管属性的读写

类委托是把整个类的方法外包出去,而属性委托的核心思想则是:将一个具体属性的读写操作,委托给另一个类去完成。

语法如下:

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

当你访问 p 时,会自动调用 Delegate 类的 getValue() 方法;当你给 p 赋值时,会自动调用 Delegate 类的 setValue() 方法。

实际上编译器会在底层自动生成一个隐藏的成员变量(比如 p$delegate),并将所有对 p 的读写请求,全部拦截并转发给这个隐藏变量的 getValue/setValue 方法。

Delegate 的实现

为了不报错,Delegate 类需要遵循固定的标准模板。首先是原生的底层写法:

kotlin 复制代码
class Delegate {
    // 类型需要和被委托的属性保持一致
    private var propValue: String = "Default"

    // 读操作
    operator fun getValue(thisRef: Any?, prop: KProperty<*>): String {
        println("getValue from $thisRef, property name is '${prop.name}'")
        return propValue
    }

    // 写操作
    operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        println("setValue to $thisRef, new value is $value")
        propValue = value
    }
}

其中:

  • thisRef : 代表持有该委托属性的老板对象实例。例如访问 MyClass 实例中的 p 属性时,它指的就是 MyClass 实例。

  • prop : 该委托属性的反射对象。我们可以通过它获取属性的元信息,例如 prop.name 获取的是当前正在操作的属性的名称。

在实际开发中,我们不会像上面那样手写,而是直接实现标准库中的 ReadOnlyPropertyReadWriteProperty 接口来完成委托。这不仅符合规范,而且利用了泛型避免参数类型写错。

kotlin 复制代码
class Delegate : ReadWriteProperty<Any?, String> {
    private var propValue: String = "Default"

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("getValue from $thisRef, property name is '${property.name}'")
        return propValue
    }

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

注意 :如果委托的属性是只读的 val,只需要实现 ReadOnlyProperty 提供 getValue 即可。

自定义实战:手写一个懒加载 (lazy)

其实我们早就用过属性委托了,不管是 Jetpack Compose 中的 val value by remember{ mutableStateOf(...) },还是 Kotlin 中的 lazy 懒加载函数(它会将属性的初始化延迟到它第一次被访问时执行)。

现在,我们完全可以自己来手写一个简化版的 later 函数:

kotlin 复制代码
/**
 * 属性委托类
 */
class Later<T>(
    /**
     * 初始化的代码块
     */
    private val block: () -> T
) : ReadOnlyProperty<Any?, T> {
    private var value: T? = null

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        // 第一次访问时,初始化属性
        if (value == null) {
            value = block()
        }

        return value!!
    }
}

/**
 * 包装成顶层函数,方便调用
 */
fun <T> later(block: () -> T) = Later(block)

fun main() {
    val p by later {
        println("initializing...")
        "Hello, later!"
    }
    println("--- Ready to visit p ---")
    println(p) // initializing...
    println("--- Visit again p ---")
    println(p) // 直接打印返回的结果
}

关键在于 by 关键字会将 p 的读写委托给 later 函数返回的 Later 对象。

通过自定义的 Later,我们实现了初始化代码块只会在第一次访问时执行。不过,在真实的项目中,我们还是要使用标准库中的 lazy 函数,因为它解决了我们没考虑到的问题:

  • 线程安全与性能控制,标准库的 lazy 还有一个 LazyThreadSafetyMode 参数,支持 SYNCHRONIZED(默认的加锁安全)、PUBLICATION(并发初始化,但只有第一个结果有效)和 NONE(无锁,性能最高但不安全)三种模式,用于各种并发场景。

  • 初始化空值处理,防止当 block 执行结果恰好为空时,下次访问委托属性又再次进入初始化过程(value == null 判断失效)。

标准库中的好用委托

除了 lazy,标准库中还提供了几个非常好用的委托工具。

观察者委托:Delegates.observable

使用场景:当属性的值发生变化时,需要自动触发一些动作(如更新 UI、打印日志)。

kotlin 复制代码
class User {
    // 初始值为 "Unspecified"
    var name: String by Delegates.observable("Unspecified") { prop, old, new ->
        println("${prop.name} property changed! old: $old -> new: $new")
        // 在这里可以通知 UI 刷新,或者更新数据库
    }
}

fun main() {
    val user = User()
    user.name = "Tony"
}

否决赋值的委托:Delegates.vetoable

使用场景:用于在赋值之前进行校验,如果不符合条件,会直接驳回,也就是拒绝赋值。

kotlin 复制代码
class Person {
    // 只有大于旧值,才能赋值成功(对应年龄只能增长)
    var age: Int by Delegates.vetoable(0) { prop, old, new ->
        new > old
    }
}

fun main() {
    val person = Person()
    person.age = 18
    println(person.age) // 18

    person.age = 10     // 无法让年龄变小
    println(person.age) // 18
}

JSON 解析神器:把属性委托给 Map

使用场景:当获取来自后端的 JSON 并解析成 Map 后,还要手动取值,此时可以将类的属性直接委托给 Map。

kotlin 复制代码
class Config(map: Map<String, Any?>) {
    // 属性名必须和 Map 中的 Key 保持一致
    val version: String by map
    val debugMode: Boolean by map
}

fun main() {
    val configMap = mapOf(
        "version" to "1.0.5",
        "debugMode" to true
    )

    val config = Config(configMap)
    println(config.version)   // 1.0.5
    println(config.debugMode) // true
}

其实这是将普通属性的获取,代理给了 Map 的扩展函数 Map.getValue()。它内部通过获取属性反射名称,作为 key 来查询 Map。

如果你希望属性是可变的,可以将其委托给 MutableMap

属性间委托:废弃字段的优雅过渡

使用场景:从 Kotlin 1.4 开始,支持将一个属性委托给另一个属性。它非常适合向后兼容,例如可以在重命名对外暴露的 SDK 字段时使用。

kotlin 复制代码
class MyConfig {
    var newName: String = "Kotlin"

    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: String by this::newName
}

fun main() {
    val config = MyConfig()
    println(config.oldName)
}

总结

Kotlin 的委托机制本质上是用于隐藏样板代码,把脏活累活丢给背后的"秘书"。

  • 当不想用高耦合的继承,但又想复用其他类的能力时,就使用类委托
  • 如果发现很多属性的 get/set 逻辑差不多(比如都要懒加载、都要观察变化、都要保存到 SharedPreferences 中)时,可以抽离成属性委托
相关推荐
pengyu1 天前
【Kotlin 协程修仙录 · 化神境 · 后阶】 | 异常天劫:Flow 异常处理的终极奥义与重试之道
android·kotlin
pengyu1 天前
【Kotlin 协程修仙录 · 化神境 · 中阶】 | 多播奥义:SharedFlow 高级配置与 Channel 的抉择之道
android·kotlin
撩得Android一次心动1 天前
Kotlin 语言【知识点整理2】
android·开发语言·kotlin
Android打工仔2 天前
Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?
android·kotlin
Android-Flutter2 天前
android Glide 源码流程分析
android·kotlin
Android-Flutter2 天前
android Glide 使用详解
android·kotlin
小陈的进阶之路2 天前
Claude Code辅助测试:API测试与pytest自动化
android·开发语言·kotlin
alexhilton3 天前
规范驱动开发:让AI生成符合预期的KMP代码
android·kotlin·android jetpack
Android-Flutter3 天前
android 性能优化 详解
android·kotlin