跳到主要内容

1 篇博文 含有标签「by lazy」

查看所有标签

Kotlin 委托属性实战:用 by 把 getter/setter 的脏活外包出去

· 阅读需 7 分钟
重庆 Kotlin User Group

你有没有在一个类里写过四五段长得几乎一模一样的代码——每次读取属性都要先判空、每次修改属性都要记日志、每次保存设置都要开编辑器?

比如"懒加载一个数据库连接",Java 风格写出来是这样的:

class Repository {
private var _db: Database? = null

val db: Database
get() {
if (_db == null) {
synchronized(this) {
if (_db == null) { // 双重检查锁
_db = Database.open()
}
}
}
return _db!!
}
}

逻辑不难,但你每写一个需要懒加载的属性,就得复制一遍这坨东西。要是再加个"值变化时要通知界面刷新"的需求,你的类会被样板代码彻底淹没。

Kotlin 的 委托属性(Property Delegation) 就是来回收这些重复劳动的。语法只有一个字符:

class Repository {
val db: Database by lazy { Database.open() }
}

完事。第一次访问 db 时才会执行 Database.open(),之后每次访问都直接返回缓存好的那个实例——线程安全由 lazy 内部处理。读写逻辑被"外包"给了 by 后面的委托对象,这就是它的全部秘密。

by 的底层约定:编译器帮你转发​

val p: T by delegate 的意思非常直白:把 p 的读取交给 delegate.getValue(...),把 p 的赋值交给 delegate.setValue(...)。

编译器实际生成的代码大致长这样:

// 你写的
class Person {
var name: String by Delegate()
}

// 编译器等价物
class Person {
private val name$delegate = Delegate()
var name: String
get() = name$delegate.getValue(this, ::name)
set(value) = name$delegate.setValue(this, ::name, value)
}

看到 this 和 ::name 了吗?委托对象会收到谁持有这个属性和属性本身的元信息(名字、类型都能拿到)。这就是为什么一个委托可以同时服务于不同名字的多个属性,还能在日志里打印出"是哪个属性变了"。

想自己写委托,只要满足这个约定即可——注意 operator 关键字不能少:

import kotlin.reflect.KProperty

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 Person {
var name: String by LoggingDelegate("张三")
var age: Int by LoggingDelegate(18)
}

fun main() {
val p = Person()
p.name = "李四" // 输出:name 从 张三 变成 李四
println(p.age) // 输出:读取 age = 18
}

val 只需要 getValue;var 需要两个都实现。写一次,全项目的属性都能复用。

标准库三个最实用的委托​

lazy:首次访问才计算​

除了懒加载对象,它还适合"配置读取""解析大文件"这类开销大的操作。lazy 默认是线程安全的(SYNCHRONIZED),单线程场景可以传 LazyThreadSafetyMode.NONE 省掉锁开销。

val config: AppConfig by lazy { loadConfigFromDisk() }

一个常见踩坑:lazy 只保证 lambda 执行一次,不保证里面的副作用只发生一次以外的东西——它缓存的是 lambda 的返回值。如果你在里面读了可变状态,结果可能和你预期的不一致。

observable:改完之后做点事​

先看不用委托时,"值一变就刷新界面"得这么手写:

class Cart {
var total: Int = 0
set(value) {
val old = field
field = value
println("total: $old → $value") // 记日志
refreshBadge(value) // 刷界面
}
}

每加一个需要监听的属性,就得重复一遍 set 块。换成委托,这套逻辑就能抽走:

import kotlin.properties.Delegates

class Cart {
// 回调参数依次是:属性本身、旧值、新值
var total: Int by Delegates.observable(0) { prop, old, new ->
println("${prop.name}: $old → $new")
refreshBadge(new)
}
}

值变化的通知逻辑集中一处,界面刷新再也不会漏调。

vetoable:改之前先否决​

observable 是"事后通知",vetoable 是"事前审批"——回调返回 false 就拒绝这次赋值,属性保持旧值:

class Session {
var level: Int by Delegates.vetoable(1) { _, _, newValue ->
newValue in 1..99 // 超出范围直接不认
}
}

fun main() {
val s = Session()
s.level = 200
println(s.level) // 1,非法值被拦下了
s.level = 5
println(s.level) // 5
}

参数校验、权限控制这类需求,从此不用在每个 setter 里写 if。