委托与委托属性¶
委托是一种把行为转交给另一个对象的设计方式。Kotlin 在语言层面支持类委托和属性委托,让组合优于继承的写法更轻量。
类委托¶
假设有一个接口:
interface Printer {
fun print(message: String)
}
class ConsolePrinter : Printer {
override fun print(message: String) {
println(message)
}
}
如果一个类想复用 Printer 的实现,可以使用 by:
class LoggingPrinter(
private val delegate: Printer
) : Printer by delegate
LoggingPrinter 会自动把 Printer 接口的方法委托给 delegate。
覆盖委托行为¶
class PrefixPrinter(
private val delegate: Printer,
private val prefix: String
) : Printer by delegate {
override fun print(message: String) {
delegate.print("$prefix$message")
}
}
你可以只覆盖需要改变的方法,其余方法继续由委托对象实现。
Java 对比:Java 中要么手写转发方法,要么使用继承、动态代理或 Lombok 等工具。Kotlin 的 by 让“组合 + 转发”变成语言特性。
委托不是继承¶
类委托表达的是“我实现这个接口,但实现细节交给另一个对象”。它不是父类继承:
- 不共享父类状态。
- 不受脆弱基类问题影响。
- 必须基于接口。
- 覆盖方法时要显式决定是否调用委托对象。
属性委托¶
属性委托把属性的读取或写入逻辑交给委托对象:
val name: String by lazy {
println("初始化")
"Kotlin"
}
第一次访问 name 时执行初始化逻辑,之后复用结果。
lazy¶
lazy 是最常用的标准库属性委托:
class ConfigLoader {
val config: Config by lazy {
loadConfigFromFile()
}
}
适用场景:
- 初始化成本较高。
- 不一定每次运行都会用到。
- 初始化依赖对象创建后的状态。
如果初始化必须立即失败,或者属性是对象核心不变量,就不要使用 lazy 隐藏初始化时机。
observable¶
监听属性变化:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("未命名") { property, oldValue, newValue ->
println("${property.name}: $oldValue -> $newValue")
}
}
每次赋新值后回调都会执行。
vetoable¶
决定是否接受新值:
import kotlin.properties.Delegates
class Account {
var balance: Int by Delegates.vetoable(0) { _, _, newValue ->
newValue >= 0
}
}
如果回调返回 false,赋值会被拒绝。
map 委托¶
可以从 Map 中读取属性:
class User(values: Map<String, Any?>) {
val name: String by values
val age: Int by values
}
val user = User(mapOf("name" to "Ada", "age" to 36))
这类写法适合处理动态配置、JSON-like 数据、脚本环境等。但在业务核心模型中,强类型构造函数通常更安全。
自定义属性委托¶
只读属性委托需要提供 getValue:
import kotlin.reflect.KProperty
class TrimmedString(private val value: String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value.trim()
}
}
class User {
val name: String by TrimmedString(" Ada ")
}
可变属性委托还需要 setValue:
class NonBlankString(initial: String) {
private var value = initial
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
require(newValue.isNotBlank()) { "${property.name} 不能为空" }
value = newValue
}
}
委托属性的代价¶
委托可以复用逻辑,但也会引入间接层:
- 调试时需要跳到委托对象。
- 过度使用会让属性行为不直观。
- 自定义委托可能影响性能和可读性。
- 属性访问可能不再是简单字段读取。
实践建议¶
- 类委托适合接口转发,优先用于组合而不是继承。
lazy适合昂贵且可延迟的只读初始化。observable、vetoable适合 UI 状态、配置状态、简单领域约束。- 核心业务模型不要过度依赖 Map 委托,强类型字段更可靠。
- 自定义委托应命名清晰,让调用者能预期属性访问行为。
参考¶
- 官方委托文档:https://kotlinlang.org/docs/delegation.html
- 官方委托属性文档:https://kotlinlang.org/docs/delegated-properties.html