2、KClass详解:成员属性、成员函数、构造函数、扩展函数与扩展属性的反射获取
好,咱们接着聊。上一章我们把KClass的基本概念和获取方式捋了一遍,这一章要动真格的了——深入KClass的内部,看看怎么通过反射拿到类里的各种成员。
说实话,反射这东西,平时写业务代码可能用得不多。但我记得有一次做插件化框架,需要在运行时动态加载某个模块里的所有API接口,那时候反射就成了救命稻草。你想想看,如果连类里有哪些方法、哪些属性都拿不到,那还谈什么动态加载?
2.1 成员属性的反射获取
先说说成员属性。在Kotlin里,属性分两种:一种是类里直接声明的,另一种是扩展出来的。我们先看第一种。
假设我们有这样一个类:
class User(val name: String, var age: Int) {
val isAdult: Boolean
get() = age >= 18
}
要拿到这个类的所有属性,用KClass的memberProperties就行:
val kClass = User::class
val properties = kClass.memberProperties
properties.forEach { prop ->
println("属性名: ${prop.name}, 类型: ${prop.returnType}")
}
输出结果:
属性名: name, 类型: kotlin.String
属性名: age, 类型: kotlin.Int
属性名: isAdult, 类型: kotlin.Boolean
嗯,这里要注意一点。memberProperties返回的是所有非扩展的属性。包括val和var,也包括那些有自定义getter的属性(比如上面的isAdult)。
memberProperties 来遍历所有属性,但如果我只想拿val(只读属性),可以用 memberProperties.filter { it is KProperty1 && !(it is KMutableProperty1) }。当然,更直接的方式是用 declaredMemberProperties 来排除继承来的属性。
2.2 成员函数的反射获取
接下来是成员函数。这个更常用——比如你要动态调用某个对象的方法,或者做AOP拦截。
还是用User类,我们加几个方法:
class User(val name: String, var age: Int) {
fun sayHello() = "Hello, I'm $name"
fun updateAge(newAge: Int) { age = newAge }
private fun secretMethod() = "这是秘密"
}
获取成员函数用memberFunctions:
val functions = User::class.memberFunctions
functions.forEach { func ->
println("函数名: ${func.name}, 参数: ${func.parameters}")
}
输出:
函数名: sayHello, 参数: [KParameter(name=null, type=User), KParameter(name=null, type=Unit)]
函数名: updateAge, 参数: [KParameter(name=null, type=User), KParameter(name=newAge, type=Int)]
函数名: secretMethod, 参数: [KParameter(name=null, type=User)]
memberFunctions 默认不包含私有函数!如果你需要获取私有成员,必须用 declaredMemberFunctions。而且要注意,即使是 declaredMemberFunctions,也只能拿到当前类声明的函数,不包括父类的。
为什么会这样?因为Kotlin的反射设计上,member*系列默认只暴露public成员。这是为了安全考虑——你总不希望随便一个框架就能调用你类的私有方法吧?
2.3 构造函数的反射获取
构造函数这块,我个人觉得是反射里最实用的功能之一。你想啊,如果不知道构造函数的参数,你怎么动态创建对象?
获取构造函数用constructors:
val constructors = User::class.constructors
constructors.forEach { constructor ->
println("构造函数参数: ${constructor.parameters}")
}
如果类有多个构造函数(比如主构造和次构造),constructors会全部返回。但大多数情况下,我们只需要主构造函数。这时候可以用primaryConstructor:
val primaryCtor = User::class.primaryConstructor
println("主构造函数参数: ${primaryCtor?.parameters}")
拿到构造函数后,怎么创建实例?用call方法:
val user = primaryCtor?.call("张三", 25)
println(user?.sayHello()) // 输出: Hello, I'm 张三
call 创建对象时,参数顺序必须和构造函数声明一致。如果参数类型不匹配,会抛出 IllegalArgumentException。我建议用 callBy 方法,它可以按参数名传值,更安全:
val params = mapOf(
primaryCtor!!.parameters[0] to "李四",
primaryCtor.parameters[1] to 30
)
val user2 = primaryCtor.callBy(params)
2.4 扩展函数的反射获取
扩展函数是Kotlin的一大特色。但反射获取扩展函数,和获取成员函数不太一样。
先定义一个扩展函数:
fun User.printInfo() = println("用户: $name, 年龄: $age")
注意,扩展函数不是类的成员。它本质上是一个静态函数,第一个参数是接收者类型。所以你不能用memberFunctions拿到它。
那怎么拿?用topLevelFunctions或者declaredFunctions?不对,这些是拿顶层函数的。扩展函数其实属于顶层函数(或者成员扩展函数),但它的接收者类型是固定的。
正确的做法是:
// 获取所有顶层函数
val topFunctions = Class.forName("com.example.MainKt").kotlin.topLevelFunctions
// 或者直接通过函数引用
val printInfoFunc = User::printInfo
println("扩展函数名: ${printInfoFunc.name}")
说白了,扩展函数在JVM层面就是一个普通的静态方法,只不过Kotlin编译器帮我们做了语法糖。所以反射获取它,和获取普通静态方法没什么两样。
XXXKt 类),然后遍历它的 declaredFunctions,再检查函数的参数列表里第一个参数是不是目标类型。虽然有点绕,但确实可行。
2.5 扩展属性的反射获取
扩展属性和扩展函数类似,也不是类的真正成员。比如:
val User.displayName: String
get() = "$name (年龄: $age)"
获取扩展属性,同样不能走memberProperties。你得通过函数引用或者顶层属性来拿:
val displayNameProp = User::displayName
println("扩展属性名: ${displayNameProp.name}, 类型: ${displayNameProp.returnType}")
或者,如果你知道扩展属性定义在哪个文件里:
val kClass = Class.forName("com.example.MainKt").kotlin
val extProps = kClass.topLevelProperties
extProps.forEach { prop ->
println("顶层属性: ${prop.name}")
}
这里有个细节:topLevelProperties返回的是文件里的所有顶层属性,包括扩展属性和普通顶层属性。你需要自己判断哪个是扩展属性——检查它的isExtension属性:
extProps.filter { it.isExtension }.forEach { extProp ->
println("扩展属性: ${extProp.name}, 接收者类型: ${extProp.receiverType}")
}
2.6 总结与对比
好了,这一章的内容有点多,我帮你整理成一个表格,方便对比记忆:
| 目标 | 获取方式 | 是否包含私有 | 是否包含扩展 |
|---|---|---|---|
| 成员属性 | memberProperties |
否 | 否 |
| 成员函数 | memberFunctions |
否 | 否 |
| 构造函数 | constructors / primaryConstructor |
是 | 不适用 |
| 扩展函数 | 函数引用 Type::funcName 或顶层函数遍历 |
取决于声明 | 是 |
| 扩展属性 | 属性引用 Type::propName 或顶层属性遍历 |
取决于声明 | 是 |
最后说一句:反射虽好,但别滥用。能用普通代码解决的问题,就别上反射。毕竟反射有性能开销,而且会让代码变得难以追踪。但当你真的需要动态处理类结构时,KClass提供的这些API就是你的瑞士军刀。
下一章,我们会深入KCallable和KParameter,看看怎么精确控制函数的调用和参数的传递。到时候你会看到,反射的威力远不止“拿到名字”这么简单。