委托与委托属性

委托是一种把行为转交给另一个对象的设计方式。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 适合昂贵且可延迟的只读初始化。
  • observablevetoable 适合 UI 状态、配置状态、简单领域约束。
  • 核心业务模型不要过度依赖 Map 委托,强类型字段更可靠。
  • 自定义委托应命名清晰,让调用者能预期属性访问行为。

参考

  • 官方委托文档:https://kotlinlang.org/docs/delegation.html
  • 官方委托属性文档:https://kotlinlang.org/docs/delegated-properties.html