4、KProperty与KMutableProperty:属性读写、委托属性的反射、属性getter/setter的调用
好,咱们继续往下走。上一章我们聊了聊 KClass 和 KCallable,算是把 Kotlin 反射的骨架搭起来了。这一章,我们来啃一块硬骨头——属性反射。
说白了,就是怎么在运行时拿到一个类的属性,怎么读它、写它,甚至怎么绕过 Kotlin 的语法糖,直接调用它的 getter 和 setter。我个人觉得,这部分是反射里最实用的,也是坑最多的。
4.1 KProperty:只读属性的“身份证”
先说说 KProperty。它代表一个不可变的属性,也就是用 val 声明的那些。
你想想看,一个属性在 Kotlin 里是什么?它背后其实是一个 getter 方法 + 一个 幕后字段(如果有的话)。KProperty 就是用来描述这个结构的。
我们来看个最简单的例子:
class User(val name: String, val age: Int)
fun main() {
val prop = User::name
println(prop.name) // 输出: name
println(prop.returnType) // 输出: kotlin.String
println(prop.isConst) // 输出: false
}
这里 User::name 返回的就是一个 KProperty1<User, String> 的实例。注意这个 1,它表示这个属性只依赖一个 receiver(接收者),也就是 User 的实例。
User::name 这种成员引用语法,得到的是 KProperty1。如果是顶层属性(比如 val PI = 3.14),得到的是 KProperty0,因为它没有 receiver。
那怎么读取这个属性的值呢?很简单,调用 get() 方法:
val user = User("Alice", 30)
val prop = User::name
val nameValue = prop.get(user) // 返回 "Alice"
println(nameValue)
嗯,这里要注意,get() 方法需要传入一个 receiver 实例。对于 KProperty0,它不需要参数,直接 get() 就行。
4.2 KMutableProperty:可写属性的“通行证”
那 var 声明的可变属性呢?它对应的是 KMutableProperty。这个接口继承自 KProperty,额外多了一个 setter 方法。
我记得刚开始用反射时,总以为 KProperty 也能写值,结果编译报错。后来才明白,Kotlin 在类型系统层面就把读写分开了,很严谨。
class MutableUser(var name: String, var age: Int)
fun main() {
val user = MutableUser("Bob", 25)
val prop = MutableUser::name
// 读取
println(prop.get(user)) // 输出: Bob
// 写入
prop.setter.call(user, "Charlie")
println(prop.get(user)) // 输出: Charlie
}
这里 prop.setter 返回的是一个 KMutableProperty.Setter 对象。你可以直接调用 call() 方法,传入 receiver 和新值。
KProperty 而不是 KMutableProperty,强行调用 setter 会抛出 UnsupportedOperationException。所以写代码前最好用 is KMutableProperty 判断一下。
4.3 属性 getter/setter 的底层调用
你可能会问:我直接用 user.name = "xxx" 不就行了,干嘛非要用反射调用 getter/setter?
嗯,这个问题问得好。在大多数业务代码里,确实没必要。但如果你在写框架、ORM、序列化工具,或者做 AOP 切面编程时,你就需要绕过编译器的语法糖,直接操作底层方法。
实际上,Kotlin 属性的 getter 和 setter 在 JVM 层面就是普通的方法。比如 val name: String 对应一个 getName() 方法,var age: Int 对应 getAge() 和 setAge(int)。
我们可以通过 KProperty 的 getter 属性拿到 KFunction,然后调用它:
val prop = User::name
val getterFunc = prop.getter
val result = getterFunc.call(user) // 等价于 prop.get(user)
对于 setter 也是一样:
val mutableProp = MutableUser::name
val setterFunc = mutableProp.setter
setterFunc.call(user, "David") // 等价于 mutableProp.setter.call(user, "David")
我个人习惯在写框架时,优先使用 prop.get(instance) 和 prop.setter.call(instance, value),而不是直接操作 getter/setter 的 KFunction。因为前者更简洁,而且 Kotlin 内部已经帮我们处理了访问权限和平台类型转换。
4.4 委托属性的反射:揭开“by”的面纱
委托属性是 Kotlin 的一大特色。比如 val x by lazy { ... } 或者 var y by Delegates.observable(...)。那反射能穿透这层委托吗?
答案是:能,但有点绕。
当你声明 val x by lazy { 42 } 时,Kotlin 编译器会生成一个 $delegate 字段,类型是 Lazy<Int>。这个字段是私有的,但反射可以访问到。
我曾经在做一个配置中心框架时,需要动态读取某个类的委托属性值。一开始我直接 prop.get(instance),发现拿到的就是最终值,而不是委托对象。后来才意识到,Kotlin 的 get() 方法已经帮我们调用了委托的 getValue()。
那如果想拿到委托对象本身呢?你需要访问那个 $delegate 字段:
class Config {
val value by lazy { computeExpensiveValue() }
}
fun main() {
val config = Config()
val delegateField = Config::class.memberProperties
.first { it.name.endsWith("\$delegate") }
delegateField.isAccessible = true
val delegate = delegateField.get(config)
println(delegate) // 输出: Lazy@...
println(delegate::class) // 输出: class kotlin.SynchronizedLazyImpl
}
$delegate 字段是编译器生成的,名称以 $delegate 结尾。通过反射访问它时,记得设置 isAccessible = true,因为它是私有的。
你想想看,这个能力在写调试工具、性能分析器或者自动化测试框架时特别有用。你可以检查某个属性是否被正确委托,甚至替换掉委托对象。
4.5 实战:一个通用的属性读写工具
说了这么多理论,我们来写一个实用的工具函数。这个函数可以读取任意对象的任意属性,支持嵌套属性路径:
fun Any.readProperty(path: String): Any? {
val parts = path.split(".")
var current: Any? = this
for (part in parts) {
if (current == null) return null
val clazz = current::class
val prop = clazz.memberProperties.firstOrNull { it.name == part }
?: return null
current = prop.get(current)
}
return current
}
// 使用示例
data class Address(val city: String, val zip: String)
data class Person(val name: String, val address: Address)
fun main() {
val person = Person("Alice", Address("Beijing", "100000"))
println(person.readProperty("name")) // 输出: Alice
println(person.readProperty("address.city")) // 输出: Beijing
println(person.readProperty("address.zip")) // 输出: 100000
}
这个工具我在好几个项目里都用过。比如在导出 Excel 时,用户配置了 "user.address.city" 这样的路径,我直接反射读取,省去了写一堆 if-else 的麻烦。
if (current == null) return null 的判断。另外,memberProperties 返回的是 Collection<KProperty1<Any?, Any?>>,类型擦除后需要手动 cast,这里用 prop.get(current) 是安全的。
4.6 性能与安全:反射不是银弹
最后,我想聊聊性能问题。反射调用属性,比直接访问慢一个数量级。为什么?因为反射需要做类型检查、访问权限检查、方法查找,甚至可能触发装箱拆箱。
我建议你遵循几个原则:
- 缓存反射对象:把
KProperty或KFunction缓存起来,不要每次调用都重新获取。 - 优先使用
get()/set():而不是call(),因为前者是类型安全的,后者需要可变参数,有额外开销。 - 只在必要的时候用反射:比如框架代码、工具类、测试代码。业务逻辑里尽量少用。
另外,安全性也要注意。通过反射可以访问私有属性,这破坏了封装性。我在做代码审查时,看到有人用反射去修改第三方库的内部状态,结果库升级后字段名变了,代码直接崩了。嗯,这种用法要谨慎。
好了,这一章的内容就到这里。下一章我们会聊聊注解的运行时处理,以及如何用注解 + 反射写出优雅的框架代码。到时候见。