核心思想:把事情交给别人做(代理)
想象一下:你(一个类 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
)。 - 如果是未初始化,加锁 (保证线程安全),执行
initializer
lambda,将结果存储在_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 // 使用同步提交确保立即写入
)
}