1. 简介

Kotlin 原生支持类属性(property)机制。通常情况下,属性会直接对应一个底层字段(field)来存储值。但其实这并非强制要求——只要属性能正确对外暴露读写行为,它就可以被视为合法的属性。

实现方式有两种:一是通过自定义 getter/setter 来控制逻辑;二是使用 委托(Delegation) 机制。后者更灵活,也更符合现代 Kotlin 编程范式,尤其适合需要复用属性逻辑的场景。

✅ 属性不一定非要绑定字段
⚠️ 不要误以为所有 var 都有 backing field


2. 什么是委托属性

简单来说:委托属性不依赖类中的字段存储数据,而是将 get 和 set 操作“委托”给另一个对象处理。这种设计可以抽离通用逻辑,实现跨多个属性甚至多个类的复用。

关键语法是 by 关键字,表示该属性的行为由右侧的委托对象接管:

class DelegateExample(map: MutableMap<String, Any?>) {
    var name: String by map
}

上面的例子利用了 MutableMap 实现了 ReadWriteProperty 接口这一特性,使得 map 的 key 可以像对象属性一样被访问和修改。

📌 踩坑提示:map 必须是构造参数传入或成员变量,不能是局部变量,否则生命周期可能出问题。


3. 标准库中的常用委托

Kotlin 标准库提供了几个开箱即用的委托类型,非常实用。

lazy:延迟初始化

只在第一次访问时计算值,并缓存结果。适用于耗时操作,比如数据库查询、复杂计算等:

class DatabaseBackedUser(userId: String) {
    val name: String by lazy {
        queryForValue("SELECT name FROM users WHERE userId = :userId", mapOf("userId" to userId))
    }
}

💡 默认线程安全(LazyThreadSafetyMode.SYNCHRONIZED),若确定单线程可用 lazy(LazyThreadSafetyMode.NONE) { ... } 提升性能。


observable:监听属性变化

当属性值改变时触发回调,常用于 UI 更新、日志记录或联动逻辑:

class ObservedProperty {
    var name: String by Delegates.observable("<not set>") { prop, old, new ->
        println("Old value: $old, New value: $new")
    }
}

⚠️ 注意:observable 不拦截赋值,仅通知变更。如果你需要阻止某些值写入,得自己在 lambda 中加判断。


vetoable:可拦截的 observable

observable 类似,但它可以在新值生效前决定是否接受:

var age: Int by Delegates.vetoable(0) { prop, old, new ->
    new >= 0 // 只允许非负数
}

✅ 属性间委托(Kotlin 1.4+)

可以直接将一个属性委托给另一个属性,特别适合 API 迁移时做兼容:

class RenamedProperty {
    var newName: String = ""

    @Deprecated("Use newName instead")
    var name: String by this::newName
}

这样调用旧属性 name 实际操作的是 newName,平滑过渡无痛升级。

📌 场景举例:重构命名、版本兼容、配置迁移。


4. 自定义委托类

当你发现标准委托不够用时,就需要手写自己的委托逻辑。核心是实现两个接口之一:

  • ReadOnlyProperty<T, R>:只读属性委托(用于 val
  • ReadWriteProperty<T, R>:可变属性委托(用于 var

🔗 Kotlin 1.4 起,ReadWriteProperty 继承自 ReadOnlyProperty,因此只需实现一个类即可通用于两种属性。

接口方法说明

方法 参数 用途
getValue(thisRef, property) thisRef: 宿主对象
property: 属性元信息(KProperty)
获取属性值
setValue(thisRef, property, value) 同上 + value: 新值 设置属性值

示例:数据库驱动的属性委托

设想我们希望某些属性直接读写数据库,而非本地内存字段:

class DatabaseDelegate<R, T>(
    private val readQuery: String,
    private val writeQuery: String,
    private val id: Any
) : ReadWriteProperty<R, T> {

    override fun getValue(thisRef: R, property: KProperty<*>): T {
        return queryForValue(readQuery, mapOf("id" to id))
    }

    override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
        update(writeQuery, mapOf("id" to id, "value" to value))
    }
}

假设存在以下顶层函数(模拟 DB 操作):

fun <T> queryForValue(sql: String, params: Map<String, Any>): T {
    // 执行查询并返回结果
    TODO("execute query")
}

fun update(sql: String, params: Map<String, Any>) {
    // 执行更新
    TODO("execute update")
}

然后就可以这样使用:

class DatabaseUser(userId: String) {
    var name: String by DatabaseDelegate(
        "SELECT name FROM users WHERE userId = :id",
        "UPDATE users SET name = :value WHERE userId = :id",
        userId
    )
    
    var email: String by DatabaseDelegate(
        "SELECT email FROM users WHERE userId = :id",
        "UPDATE users SET email = :value WHERE userId = :id",
        userId
    )
}

✅ 优势:

  • 数据一致性高(始终从 DB 读写)
  • 透明化访问,调用方无需感知底层逻辑
  • 易于测试和替换实现

❌ 缺点:

  • 性能开销大(每次读写都走 DB)
  • 异常处理需谨慎(网络/SQL 错误)

📌 建议结合 lazy 或缓存机制优化高频读取场景。


5. 委托创建的动态控制(Delegate Provider)

Kotlin 1.4 引入了 PropertyDelegateProvider 接口,允许我们在属性声明时动态决定使用哪个具体委托实例。

这个机制的核心是:把 delegate 的创建过程也委托出去

使用场景

  • 根据属性是否可空选择不同逻辑
  • 按注解配置行为
  • 日志/监控委托初始化过程

示例:根据类型是否可空选择不同委托

class DatabaseDelegateProvider<R, T>(
    private val readQuery: String,
    private val writeQuery: String,
    private val id: Any
) : PropertyDelegateProvider<R, ReadWriteProperty<R, T>> {

    override operator fun provideDelegate(
        thisRef: R,
        prop: KProperty<*>
    ): ReadWriteProperty<R, T> {
        return if (prop.returnType.isMarkedNullable) {
            NullableDatabaseDelegate(readQuery, writeQuery, id)
        } else {
            NonNullDatabaseDelegate(readQuery, writeQuery, id)
        }
    }
}

接着你可以这样声明属性:

class User(userId: String) {
    var name: String by DatabaseDelegateProvider(
        "SELECT name FROM users WHERE id = :id",
        "UPDATE users SET name = :value WHERE id = :id",
        userId
    )

    var nickname: String? by DatabaseDelegateProvider(
        "SELECT nickname FROM users WHERE id = :id",
        "UPDATE users SET nickname = :value WHERE id = :id",
        userId
    )
}

此时,nickname 是可空的,就会自动使用 NullableDatabaseDelegate,而 name 使用非空版本。

📌 优势:

  • 分离关注点:每个委托只处理特定情况
  • 更细粒度控制
  • 提升代码可维护性

⚠️ 注意:provideDelegate 在属性初始化阶段调用一次,之后不再执行。


6. 总结

委托属性是 Kotlin 中极具表现力的功能之一,它让开发者能够以声明式的方式封装复杂的属性逻辑,同时保持调用侧简洁自然。

✅ 典型应用场景包括:

  • 延迟加载(lazy
  • 属性监听(observable / vetoable
  • 外部存储映射(DB、SharedPreferences、Map 等)
  • 动态行为切换(通过 PropertyDelegateProvider

📌 关键要点回顾:

  • 使用 by 关键字启用委托
  • 标准库提供常用委托,避免重复造轮子
  • 自定义委托需实现 ReadWritePropertyReadOnlyProperty
  • Kotlin 1.4+ 支持属性间委托和 delegate provider 机制
  • 尽量避免在委托中引入副作用或性能瓶颈

完整示例代码已托管至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-lang-oop-2


原始标题:Delegated Properties in Kotlin