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)

我们可以通过 KPropertygetter 属性拿到 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 的麻烦。

避坑指南: 我曾经在读取嵌套属性时,忘记处理中间某个属性为 null 的情况,结果抛出了 NullPointerException。所以上面代码里我加了 if (current == null) return null 的判断。另外,memberProperties 返回的是 Collection<KProperty1<Any?, Any?>>,类型擦除后需要手动 cast,这里用 prop.get(current) 是安全的。

4.6 性能与安全:反射不是银弹

最后,我想聊聊性能问题。反射调用属性,比直接访问慢一个数量级。为什么?因为反射需要做类型检查、访问权限检查、方法查找,甚至可能触发装箱拆箱。

我建议你遵循几个原则:

  • 缓存反射对象:把 KPropertyKFunction 缓存起来,不要每次调用都重新获取。
  • 优先使用 get() / set():而不是 call(),因为前者是类型安全的,后者需要可变参数,有额外开销。
  • 只在必要的时候用反射:比如框架代码、工具类、测试代码。业务逻辑里尽量少用。

另外,安全性也要注意。通过反射可以访问私有属性,这破坏了封装性。我在做代码审查时,看到有人用反射去修改第三方库的内部状态,结果库升级后字段名变了,代码直接崩了。嗯,这种用法要谨慎。

好了,这一章的内容就到这里。下一章我们会聊聊注解的运行时处理,以及如何用注解 + 反射写出优雅的框架代码。到时候见。