14、委托:类委托、属性委托、标准委托(lazy、observable、vetoable)
委托模式,说白了就是「我懒得干,你替我干」。在 Kotlin 里,这个模式被提升到了语言层面,用关键字 by 就能搞定。我个人觉得,这是 Kotlin 最实用的语法糖之一,能帮你省掉大量样板代码。
今天咱们就把它拆开揉碎了讲清楚。分成三块:类委托、属性委托、还有 Kotlin 标准库自带的几个委托工具。
14.1 类委托:把实现甩给「别人」
先看一个场景。你有一个接口,比如 Logger:
interface Logger {
fun log(message: String)
fun logError(message: String)
}
你写了一个 ConsoleLogger 实现了它。现在你想加一个 FileLogger,但大部分逻辑和 ConsoleLogger 一样,只是输出方式不同。传统做法是写一个代理类,手动转发所有方法——太累了。
Kotlin 的类委托就是干这个的:
class FileLogger(private val consoleLogger: ConsoleLogger) : Logger by consoleLogger {
override fun logError(message: String) {
// 只覆盖这一个方法
println("写入文件: $message")
}
}
看到 by consoleLogger 了吗?意思是:除了你显式 override 的方法,其他所有 Logger 接口的方法,都交给 consoleLogger 去执行。
14.2 属性委托:把读写权交出去
属性委托更常用。语法是 val/var 属性名 by 委托对象。委托对象需要实现 getValue 和 setValue 操作符。
举个例子,你有一个 User 类,里面的 name 属性想从数据库或 SharedPreferences 里读取:
class User {
var name: String by PreferenceDelegate("name", "默认名字")
}
这个 PreferenceDelegate 长什么样?
class PreferenceDelegate<T>(private val key: String, private val default: T) {
private val prefs = getSharedPreferences() // 假设有这个方法
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
// 从 prefs 读取
return prefs.getString(key, default as String) as T
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
prefs.edit().putString(key, value as String).apply()
}
}
嗯,这里要注意:getValue 和 setValue 是操作符重载,必须用 operator 关键字。而且 KProperty 参数能让你拿到属性名、类型等元信息,非常强大。
14.3 标准委托:Kotlin 自带的三个神器
Kotlin 标准库提供了三个非常实用的委托:lazy、observable、vetoable。我几乎每个项目都会用到它们。
14.3.1 lazy:懒加载,用的时候才创建
这个最简单,也最常用。比如你有一个很重的对象,不想在类初始化时就创建:
val heavyObject: HeavyObject by lazy {
println("第一次访问时才会执行这里")
HeavyObject()
}
第一次访问 heavyObject 时,lazy 后面的 lambda 才会执行。之后访问,直接返回缓存的结果。
lazy 还有三个模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
LazyThreadSafetyMode.SYNCHRONIZED |
线程安全,默认模式 | 多线程环境 |
LazyThreadSafetyMode.NONE |
不加锁,性能最高 | 单线程或确定不会并发 |
LazyThreadSafetyMode.PUBLICATION |
允许并发初始化,但只取第一个结果 | 对结果一致性要求不高 |
lazy 加载一个网络请求结果,结果因为线程安全问题,两个线程同时触发了初始化,导致请求发了两次。后来改成 SYNCHRONIZED 才解决。所以,除非你确定是单线程,否则别用 NONE。
14.3.2 observable:监听属性变化
你想在属性被赋值时干点额外的事,比如更新 UI 或打印日志:
var name: String by Delegates.observable("初始值") { property, oldValue, newValue ->
println("属性 ${property.name} 从 $oldValue 变成了 $newValue")
}
每次 name 被赋值,lambda 都会执行。注意,observable 是在赋值之后回调,你无法阻止这次赋值。
我在项目中用它来做数据绑定。比如一个用户信息页面,name 变了,自动刷新对应的 TextView:
var userName: String by Delegates.observable("") { _, _, newValue ->
nameTextView.text = newValue
}
省掉了手动调 updateUI() 的麻烦。
14.3.3 vetoable:带否决权的监听
这个和 observable 很像,但多了一个「否决」能力。回调返回 true 表示允许赋值,false 表示拒绝:
var age: Int by Delegates.vetoable(0) { _, _, newValue ->
// 年龄不能为负数
newValue >= 0
}
试试看:
age = 25 // 允许,age 变成 25
age = -5 // 拒绝,age 还是 25
vetoable 的回调是在赋值之前执行的。如果返回 false,新值根本不会写入。这和 observable 的执行时机正好相反。
我曾经用 vetoable 做过一个表单校验。用户输入手机号时,只允许数字,超过 11 位就拒绝:
var phone: String by Delegates.vetoable("") { _, _, newValue ->
newValue.all { it.isDigit() } && newValue.length <= 11
}
你看,代码简洁又直观。
14.4 知识体系总览
为了让你更直观地理解委托的整体结构,我画了一张图:
14.5 总结
委托这个东西,说白了就是「把活外包」。Kotlin 用 by 关键字把这个模式做得极其优雅。
- 类委托:适合接口实现复用,避免手写转发代码。
- 属性委托:适合把属性的存取逻辑抽离,比如 SharedPreferences、数据库字段。
- 标准委托:
lazy做懒加载,observable做监听,vetoable做带否决的监听。
我个人建议,刚开始用的时候,先从 lazy 入手,它最直观。然后试试 observable 做 UI 绑定。等你熟悉了,再自己写自定义委托,你会发现很多重复代码都能被它干掉。
嗯,委托就讲到这里。记住一句话:能用 by 解决的问题,就别自己手写。
公众号:蓝海资料掘金营,微信deep3321