委托机制 (Delegation)
源:Delegation & Delegated properties
委托是一种强大的设计模式,它是“组合优于继承”理念的终极体现。Kotlin 通过 by 关键字在语言层面原生支持了类委托和属性委托,极大减少了装饰器模式和字段存取逻辑的样板代码。
类委托 (Class Delegation)
类委托允许你将接口的所有公共成员委托给一个指定的对象。这使得你可以轻松实现装饰器模式,而无需手动转发每一个方法。
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)来处理。
语法格式:
val/var <属性名>: <类型> by <委托表达式>by 关键字后面的表达式就是委托对象。
工作原理
当编译器遇到 by 时,它会生成隐藏的辅助属性。
- 读取属性 (
get()) ➔ 调用委托对象的getValue()。 - 写入属性 (
set()) ➔ 调用委托对象的setValue()。
自定义委托详解
要创建一个委托,你不需要继承任何特殊的类,只需遵循 Kotlin 的操作符约定。
手写 Operator 方式
这是最基础的方式,只需定义 getValue(对于 var 还需要 setValue)。
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 标准库提供了 ReadOnlyProperty 和 ReadWriteProperty 接口。
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) {
// ...
}
}参数深度解析
getValue 和 setValue 接收两个关键参数:
- thisRef: 发起调用的对象。
- 它的类型决定了该委托可以被用在哪些类中。
- 如果定义为
Any?,则该委托通用。 - 如果定义为
Activity,则该委托只能在 Activity(或其子类)中使用。
- property: 属性的元数据。
- 类型为
KProperty<*>。 - 可以获取属性名 (
name)、注解、返回类型等信息。
- 类型为
// 示例:限制只能在 Activity 中使用的委托
class ActivityDelegate : ReadOnlyProperty<Activity, String> {
override fun getValue(thisRef: Activity, property: KProperty<*>): String {
return thisRef.localClassName // 访问 Activity 特有的属性
}
}标准库内置委托
lazy (延迟初始化)
只有在第一次访问时才计算值,并缓存结果。它不仅适用于成员属性,也广泛用于局部变量。
val lazyValue: String by lazy {
println("Computed!")
"Hello"
}observable (观察者)
当属性发生变化时,触发回调。
var name: String by Delegates.observable("<no name>") { prop, old, new ->
println("$old -> $new")
}vetoable (可否决的观察者)
允许你根据新值决定是否允许修改。
var age: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0 // 只有非负数才会被允许赋值
}Map 委托
Kotlin 标准库允许直接将 Map 实例作为委托对象。这在处理 JSON 解析或动态配置时非常有用。
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 演进过程中,我们经常需要重命名属性,但为了保持兼容性,不能直接删除旧属性。此时,可以将旧属性直接委托给新属性。
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 编译器到底做了什么?
字节码剖析
编译器会为每个委托属性生成一个辅助属性:
- 生成一个名为
prop$delegate的私有字段来存储委托对象。 - 在
getter中调用委托对象的getValue方法。 getValue接收两个参数:thisRef(属性所属的实例)和property(KProperty<*>类型的反射元数据)。
进阶:使用 provideDelegate 拦截创建
标准的 getValue 只有在访问属性时才会被调用。如果你希望在属性定义时就进行校验(例如检查属性名是否符合规范),或者根据元数据动态决定返回哪个委托实例,需要实现 provideDelegate 操作符。
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 是更规范的做法。
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)中生效。
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在运行时获取属性名、类型等信息。