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
关键字启用委托 - 标准库提供常用委托,避免重复造轮子
- 自定义委托需实现
ReadWriteProperty
或ReadOnlyProperty
- Kotlin 1.4+ 支持属性间委托和 delegate provider 机制
- 尽量避免在委托中引入副作用或性能瓶颈
完整示例代码已托管至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-lang-oop-2