"委托"是一种非常经典的设计模式,它的基本理念很简单,就是一个对象不自己去处理某个请求,而是将请求转交给另一个辅助对象去处理。
这就好比"老板与秘书"的关系:老板在接到单后,会把活都揽给秘书(辅助对象)去做,自己当甩手掌柜。
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获取的是当前正在操作的属性的名称。
在实际开发中,我们不会像上面那样手写,而是直接实现标准库中的 ReadOnlyProperty 或 ReadWriteProperty 接口来完成委托。这不仅符合规范,而且利用了泛型避免参数类型写错。
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 中)时,可以抽离成属性委托。