核心思想:把事情交给别人做(代理)
想象一下:你(一个类 A)需要完成一项任务(实现一个接口 I 或者管理一个属性 p 的读取/写入逻辑),但你不想亲自做所有细节。你可以找一个助手(委托对象 delegate),把这项任务委托给它去完成。
Kotlin 通过关键字 by 在语言层面优雅地支持了两种主要的委托模式:
- 类委托: 把实现接口的责任委托给另一个对象。
- 属性委托: 把属性的读取 (
get()) 和写入 (set()) 逻辑委托给另一个对象。
一、类委托:让别人替你干活(实现接口)
场景: 你需要实现一个接口 Printer,但你发现已经有一个现成的类 LaserPrinter 完美实现了这个接口。你不想重新写一遍代码,也不想直接继承 LaserPrinter(可能因为 Kotlin 是单继承,或者 LaserPrinter 是 final 类)。
如何用 (by):
kotlin
// 1. 定义一个接口
interface Printer {
fun print(message: String)
}
// 2. 一个已经实现了该接口的类
class LaserPrinter : Printer {
override fun print(message: String) {
println("激光打印机滋滋滋...打印:$message")
}
}
// 3. 你自己的类,通过 `by` 将 Printer 接口的实现委托给 laserPrinter 对象
class OfficePrinter(private val laserPrinter: LaserPrinter) : Printer by laserPrinter {
// 你可以选择什么都不写,OfficePrinter 就有了 print 方法
// 或者,你可以覆盖/添加自己的方法
fun copyDocument(document: String) {
println("正在复印:$document")
print(document) // 调用委托的 print 方法
}
}
// 使用
fun main() {
val laser = LaserPrinter()
val officePrinter = OfficePrinter(laser)
officePrinter.print("季度报告") // 输出:激光打印机滋滋滋...打印:季度报告
officePrinter.copyDocument("合同") // 输出:正在复印:合同 \n 激光打印机滋滋滋...打印:合同
}
-
OfficePrinter : Printer by laserPrinter: 这行是关键。它告诉编译器:OfficePrinter实现了Printer接口。- 但是 ,所有
Printer接口方法的具体实现 ,都交给构造函数传入的laserPrinter对象去处理。
-
在
main中调用officePrinter.print(...)时,实际上调用的就是laserPrinter.print(...)。
原理 (编译器魔法):
编译器在背后帮你生成了"胶水代码"。想象一下它大致为你生成了这样的代码:
kotlin
class OfficePrinter(private val laserPrinter: LaserPrinter) : Printer {
// 编译器自动为每个 Printer 接口的方法生成实现,并转发给 delegate
override fun print(message: String) {
laserPrinter.print(message) // 核心:转发调用!
}
// ... 你自己的 copyDocument 方法 ...
}
优点:
- 避免继承限制: Kotlin 单继承,委托可以让你"复用"多个类的功能。
- 解耦:
OfficePrinter不需要知道LaserPrinter内部怎么实现print,它只依赖接口。 - 灵活: 可以轻松替换不同的委托对象(比如换成
InkjetPrinter),只要它们实现了相同的接口。 - 控制: 你可以在委托类中覆盖部分方法,添加自己的逻辑,或者只委托部分方法。
二、属性委托:让别人管你的"钱袋子"(属性存取逻辑)
场景: 你有一个属性,但它的获取 (get()) 或设置 (set()) 逻辑可能很复杂(比如延迟初始化、数据校验、日志记录、存储到数据库/SharedPreferences、监听变化等)。你想把这套逻辑抽出来复用,避免在每个属性里写重复代码。
如何用 (by) + 标准委托例子:
Kotlin 标准库提供了几个开箱即用的属性委托:
-
lazy: 延迟初始化 (最常用!)kotlinclass MyActivity : AppCompatActivity() { // 第一次访问 expensiveView 时才会执行 lambda 初始化它 private val expensiveView: MyCustomView by lazy { println("正在初始化昂贵的视图...") findViewById(R.id.expensive_view) // 假设这个 findViewById 开销很大 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 第一次访问时初始化 expensiveView.doSomething() // 输出:"正在初始化昂贵的视图..." 然后执行 doSomething // 后续再访问 expensiveView,直接返回初始化好的结果,不会再次执行 lambda } }by lazy { ... }: 委托给Lazy<T>的一个实例。- 原理: Lambda 只在第一次读取 该属性时执行一次,结果被缓存。后续所有读取都直接返回缓存的值。默认线程安全 (
LazyThreadSafetyMode.SYNCHRONIZED)。
-
observable: 监听属性变化kotlinclass User { var name: String by Delegates.observable("<无名>") { property, oldValue, newValue -> println("属性 `${property.name}` 从 `$oldValue` 变成了 `$newValue`") } } fun main() { val user = User() user.name = "Alice" // 输出:属性 `name` 从 `<无名>` 变成了 `Alice` user.name = "Bob" // 输出:属性 `name` 从 `Alice` 变成了 `Bob` }by Delegates.observable(initialValue) { ... }: 委托给一个特殊的ReadWriteProperty对象。- 原理: 每当通过
=给属性赋值时,在赋值之后,会自动调用你提供的 lambda,告诉你属性名、旧值、新值。
-
vetoable: 拦截属性赋值 (类似observable,但在赋值前调用,可以否决) -
notNull: 提供非空检查 (早期常用,现在直接用 lateinit var 更普遍) -
Map委托: 属性值存储在 Map 中 (常用于解析 JSON 或配置)kotlindata class Config(val map: Map<String, Any?>) { val serverUrl: String by map // 从 map 中根据属性名 "serverUrl" 取 String 值 val port: Int by map // 从 map 中根据属性名 "port" 取 Int 值 val debugMode: Boolean by map } fun main() { val configMap = mapOf( "serverUrl" to "https://api.example.com", "port" to 8080, "debugMode" to true ) val config = Config(configMap) println(config.serverUrl) // 输出:https://api.example.com println(config.port) // 输出:8080 }
自定义属性委托:
当标准库不满足需求时,你可以自己造轮子。核心是遵循约定:
- 只读属性 (
val): 委托类需要提供一个operator fun getValue(thisRef: R, property: KProperty<*>): T函数。 - 读写属性 (
var): 委托类除了getValue,还需要提供一个operator fun setValue(thisRef: R, property: KProperty<*>, value: T)函数。
thisRef: 包含该属性的对象的引用(如果是扩展属性,则是接收者类型)。类型R必须是属性所有者的类型或其超类型。property: 被委托的属性本身的元信息(如名称)。类型是KProperty<*>或其超类型。value: 要设置的新值(仅在setValue中)。类型T必须与属性类型相同或其超类型。
例子:一个简单的日志记录委托
kotlin
class LoggingDelegate<T>(private var value: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("读取属性 `${property.name}` = $value")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
println("设置属性 `${property.name}`: $value -> $newValue")
value = newValue
}
}
class User {
var age: Int by LoggingDelegate(0)
}
fun main() {
val user = User()
user.age = 25 // 输出:设置属性 `age`: 0 -> 25
println(user.age) // 输出:读取属性 `age` = 25 \n 25
}
属性委托原理 (编译器魔法):
当你写 val/var someProperty: Type by MyDelegate() 时,编译器大致做以下事情:
-
创建一个委托对象的实例:
private val myDelegate$delegate = MyDelegate()(名字会修饰以避免冲突)。 -
为属性生成访问器:
-
对于
val(只读):kotlinfun getSomeProperty(): Type { return myDelegate$delegate.getValue(this, this::someProperty) } -
对于
var(读写):kotlinfun getSomeProperty(): Type { return myDelegate$delegate.getValue(this, this::someProperty) } fun setSomeProperty(value: Type) { myDelegate$delegate.setValue(this, this::someProperty, value) }
-
源码调用追踪 (以 lazy 为例,简化版):
-
val myVar: T by lazy { initializer } -
编译器生成一个
Lazy类型的委托属性实例:private val myVar$delegate = LazyKt.lazy(initializer)。LazyKt.lazy()是工厂函数。 -
常见的
lazy实现是SynchronizedLazyImpl。 -
当你第一次访问
myVar:-
编译器生成的
getMyVar()被调用:return myVar$delegate.getValue(this, this::myVar) -
SynchronizedLazyImpl.getValue(...)被调用:- 检查内部标记
_value(初始是UNINITIALIZED_VALUE)。 - 如果是未初始化,加锁 (保证线程安全),执行
initializerlambda,将结果存储在_value中,并标记为已初始化,释放锁。 - 返回
_value存储的值。
- 检查内部标记
-
-
后续访问直接返回存储的值。
总结:Kotlin 委托的精髓
-
by关键字: 这是委托的标记。 -
类委托 (
by+ 对象):- 目的:将接口实现的责任转交给另一个对象。
- 本质:编译器自动为接口方法生成转发调用。
- 优点:代码复用、解耦、避免继承限制。
-
属性委托 (
by+ 委托对象实例):- 目的:将属性的
get/set逻辑封装到可复用的委托对象中。 - 核心约定:委托对象必须实现
getValue(对于val/var) 和setValue(对于var) 操作符函数。 - 标准库好帮手:
lazy(延迟初始化),observable(监听变化),vetoable(拦截赋值),map(映射存储) 等。 - 自定义委托:遵循
getValue/setValue约定实现自己的逻辑。 - 原理:编译器生成访问器方法,这些方法内部调用委托对象的
getValue/setValue。
- 目的:将属性的
-
Android 中的高频应用:
by viewModels(): 委托给ViewModelProvider来获取或创建ViewModel实例 (本质是属性委托)。by lazy: 延迟初始化View(避免findViewById在onCreate中过早执行)、其他开销大的资源。- 属性委托到
SharedPreferences/DataStore: 封装存储逻辑。 - 类委托: 复用实现,比如将
RecyclerView.Adapter的部分职责委托给ListAdapter或DiffUtil相关的辅助类。
示例 :SharedPreferences 属性委托
kotlin
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* SharedPreferences 属性委托
*
* @param prefs SharedPreferences 实例
* @param key 存储键名(可选,默认使用属性名)
* @param defaultValue 默认值(必须提供)
* @param commit 是否使用 commit 同步提交(默认使用 apply 异步)
*/
class SafePrefsDelegate<T : Any>(
private val prefs: SharedPreferences,
private val key: String? = null,
private val defaultValue: T,
private val commit: Boolean = false
) : ReadWriteProperty<Any, T> {
init {
// 验证支持的类型
val supportedTypes = listOf(
String::class,
Int::class,
Boolean::class,
Long::class,
Float::class,
Set::class
)
require(defaultValue::class in supportedTypes) {
"Unsupported type: ${defaultValue::class.java.simpleName}. " +
"Supported types: String, Int, Boolean, Long, Float, Set<String>"
}
// 验证 Set 类型必须是 Set<String>
if (defaultValue is Set<*>) {
require(defaultValue.all { it is String }) {
"Set must contain only String values"
}
}
}
override fun getValue(thisRef: Any, property: KProperty<*>): T {
val usedKey = key ?: property.name
return when (defaultValue) {
is String -> prefs.getString(usedKey, defaultValue) as T
is Int -> prefs.getInt(usedKey, defaultValue) as T
is Boolean -> prefs.getBoolean(usedKey, defaultValue) as T
is Long -> prefs.getLong(usedKey, defaultValue) as T
is Float -> prefs.getFloat(usedKey, defaultValue) as T
is Set<*> -> prefs.getStringSet(usedKey, defaultValue as Set<String>) as T
else -> throw IllegalStateException("Unhandled type")
}
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
val usedKey = key ?: property.name
val editor = prefs.edit()
when (value) {
is String -> editor.putString(usedKey, value)
is Int -> editor.putInt(usedKey, value)
is Boolean -> editor.putBoolean(usedKey, value)
is Long -> editor.putLong(usedKey, value)
is Float -> editor.putFloat(usedKey, value)
is Set<*> -> {
require(value.all { it is String }) { "Set must contain only String values" }
@Suppress("UNCHECKED_CAST")
editor.putStringSet(usedKey, value as Set<String>)
}
else -> throw IllegalStateException("Unhandled type")
}
if (commit) {
editor.commit()
} else {
editor.apply()
}
}
}
// 扩展函数简化使用
fun <T : Any> SharedPreferences.safePrefs(
key: String? = null,
defaultValue: T,
commit: Boolean = false
): SafePrefsDelegate<T> {
return SafePrefsDelegate(this, key, defaultValue, commit)
}
// 使用示例
class AppSettings(private val prefs: SharedPreferences) {
// 使用属性名作为键名
var userId: String by prefs.safePrefs(defaultValue = "")
// 显式指定键名
var darkModeEnabled: Boolean by prefs.safePrefs(
key = "dark_mode",
defaultValue = false
)
// 数值类型
var loginCount: Int by prefs.safePrefs(defaultValue = 0)
// 浮点数
var volumeLevel: Float by prefs.safePrefs(defaultValue = 0.8f)
// 长整型(时间戳)
var lastLogin: Long by prefs.safePrefs(defaultValue = 0L)
// 字符串集合
var favoriteCategories: Set<String> by prefs.safePrefs(
defaultValue = setOf("news", "sports")
)
// 同步提交的配置项
var criticalConfig: String by prefs.safePrefs(
defaultValue = "default",
commit = true // 使用同步提交确保立即写入
)
}