Skip to content

委托机制 (Delegation)

源:Delegation & Delegated properties

委托是一种强大的设计模式,它是“组合优于继承”理念的终极体现。Kotlin 通过 by 关键字在语言层面原生支持了类委托和属性委托,极大减少了装饰器模式和字段存取逻辑的样板代码。

类委托 (Class Delegation)

类委托允许你将接口的所有公共成员委托给一个指定的对象。这使得你可以轻松实现装饰器模式,而无需手动转发每一个方法。

kotlin
interface Base {
    fun print()
}

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

// Derived 实现了 Base,但将其逻辑委托给 b
class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print() // 输出 10
}

覆盖委托成员

如果 Derived 自己重写了接口方法,那么它将使用自己的实现,而不会去调用委托对象的方法。

委托陷阱:this 的指向

类委托本质上是静态转发。委托对象 (b) 内部的 this 依然指向它自己,而不是外层的 Derived。 如果 BaseImpl 内部有一个方法调用了 print(),它永远会调用 BaseImpl 自己的 print(),即使 Derived 覆盖了 print() 也没用。这与继承的多态行为完全不同。

属性委托 (Property Delegation)

属性委托的核心思想是:属性的 getter(和 setter)逻辑不是在属性内部定义的,而是委托给另一个对象(Helper Object)来处理。

语法格式:

kotlin
val/var <属性名>: <类型> by <委托表达式>

by 关键字后面的表达式就是委托对象

工作原理

当编译器遇到 by 时,它会生成隐藏的辅助属性。

  • 读取属性 (get()) ➔ 调用委托对象的 getValue()
  • 写入属性 (set()) ➔ 调用委托对象的 setValue()

自定义委托详解

要创建一个委托,你不需要继承任何特殊的类,只需遵循 Kotlin 的操作符约定

手写 Operator 方式

这是最基础的方式,只需定义 getValue(对于 var 还需要 setValue)。

kotlin
class SimpleDelegate {
    private var storedValue: String = "Default"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Log: '${property.name}' has been read.")
        return storedValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Log: '${property.name}' set to '$value'.")
        storedValue = value
    }
}

class User {
    var name: String by SimpleDelegate()
}

标准接口方式 (推荐)

为了避免手写函数签名出错,Kotlin 标准库提供了 ReadOnlyPropertyReadWriteProperty 接口。

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class StandardDelegate : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Value"
    }

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

参数深度解析

getValuesetValue 接收两个关键参数:

  • thisRef: 发起调用的对象
    • 它的类型决定了该委托可以被用在哪些类中。
    • 如果定义为 Any?,则该委托通用。
    • 如果定义为 Activity,则该委托只能在 Activity(或其子类)中使用。
  • property: 属性的元数据
    • 类型为 KProperty<*>
    • 可以获取属性名 (name)、注解、返回类型等信息。
kotlin
// 示例:限制只能在 Activity 中使用的委托
class ActivityDelegate : ReadOnlyProperty<Activity, String> {
    override fun getValue(thisRef: Activity, property: KProperty<*>): String {
        return thisRef.localClassName // 访问 Activity 特有的属性
    }
}

标准库内置委托

lazy (延迟初始化)

只有在第一次访问时才计算值,并缓存结果。它不仅适用于成员属性,也广泛用于局部变量

kotlin
val lazyValue: String by lazy {
    println("Computed!")
    "Hello"
}

observable (观察者)

当属性发生变化时,触发回调。

kotlin
var name: String by Delegates.observable("<no name>") { prop, old, new ->
    println("$old -> $new")
}

vetoable (可否决的观察者)

允许你根据新值决定是否允许修改。

kotlin
var age: Int by Delegates.vetoable(0) { _, _, new ->
    new >= 0 // 只有非负数才会被允许赋值
}

Map 委托

Kotlin 标准库允许直接将 Map 实例作为委托对象。这在处理 JSON 解析或动态配置时非常有用。

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

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))
println(user.name) // John Doe

如果是 MutableMap,则可以支持 var 属性的读写。

属性重命名与委托 Kotlin 1.4+

在 API 演进过程中,我们经常需要重命名属性,但为了保持兼容性,不能直接删除旧属性。此时,可以将旧属性直接委托给新属性。

kotlin
class User {
    var newName: String = "Viro"
    
    @Deprecated("Use newName instead", ReplaceWith("newName"))
    var oldName: String by ::newName // 直接将操作转发给 newName
}

val user = User()
user.oldName = "New Viro" // 实际修改的是 newName
println(user.newName) // 输出 "New Viro"

这种方式避免了手写 getter/setter 的转发逻辑,且能正确处理 KProperty 的元数据。

底层原理:KProperty 的魔力

当你声明一个委托属性时,Kotlin 编译器到底做了什么?

字节码剖析

编译器会为每个委托属性生成一个辅助属性:

  1. 生成一个名为 prop$delegate 的私有字段来存储委托对象。
  2. getter 中调用委托对象的 getValue 方法。
  3. getValue 接收两个参数:thisRef(属性所属的实例)和 propertyKProperty<*> 类型的反射元数据)。

进阶:使用 provideDelegate 拦截创建

标准的 getValue 只有在访问属性时才会被调用。如果你希望在属性定义时就进行校验(例如检查属性名是否符合规范),或者根据元数据动态决定返回哪个委托实例,需要实现 provideDelegate 操作符。

kotlin
class CheckNameDelegate {
    operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadOnlyProperty<Any?, String> {
        // 在属性创建时立即执行检查
        if (!prop.name.startsWith("config")) {
            throw IllegalArgumentException("Config property '${prop.name}' must start with 'config'")
        }
        // 返回真正的委托对象
        return object : ReadOnlyProperty<Any?, String> {
            override fun getValue(thisRef: Any?, property: KProperty<*>): String = "Valid Value"
        }
    }
}

实战:封装 SharedPreferences 委托

通过属性委托,我们可以像操作普通变量一样操作持久化数据。虽然可以手写 operator,但实现标准接口 ReadWriteProperty 是更规范的做法。

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class PreferenceDelegate<T>(
    private val key: String, 
    private val default: T
) : ReadWriteProperty<Any?, T> {
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        // 模拟读取逻辑:prefs.getString(key, default)
        return default
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        // 模拟写入逻辑:prefs.edit().putString(key, value).apply()
        println("Saving $key = $value")
    }
}

// 使用
var userToken: String by PreferenceDelegate("token", "")

扩展函数形式的委托

有时我们希望委托逻辑只在特定的上下文(如 Android Context)中生效。

kotlin
import kotlin.properties.ReadOnlyProperty

// 定义一个只读属性委托
fun Context.stringResource(resId: Int): ReadOnlyProperty<Context, String> {
    return ReadOnlyProperty { thisRef, _ ->
        thisRef.getString(resId)
    }
}

// 在 Activity 中使用
class MainActivity : Activity() {
    val title by stringResource(R.string.title)
}

总结

  • 类委托:快速构建装饰器,拥抱组合。
  • 属性委托:统一处理属性的读写逻辑(缓存、校验、持久化)。
  • lazy 是标配:利用它优化昂贵对象的创建。
  • 元数据访问:利用 KProperty 在运行时获取属性名、类型等信息。