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,看看怎么精确控制函数的调用和参数的传递。到时候你会看到,反射的威力远不止“拿到名字”这么简单。