2. Kotlin反射核心API:KProperty、KFunction、KParameter、KType的详细用法

反射这东西,说白了就是让代码在运行时能“看”到自己。我刚开始接触Kotlin反射时,觉得它跟Java反射差不多,后来踩了几个坑才发现——嗯,Kotlin的反射设计得更优雅,但也更讲究套路。

今天咱们就把四个核心API掰开揉碎:KPropertyKFunctionKParameterKType。你想想看,搞懂它们,注解处理器、依赖注入、序列化这些高级玩法,你就有了底气。

Kotlin反射核心API体系 Kotlin反射核心 KProperty 属性反射:get/set/注解 KFunction 函数反射:调用/参数/返回 KParameter 参数反射:名称/类型/可选 KType 类型反射:泛型/可空/子类型 四个API协同工作,覆盖Kotlin反射的完整能力

2.1 KProperty:属性的“身份证”

KProperty 代表一个属性。在Java里你要用 Field 来搞,但Kotlin的属性比字段更“高级”——它自带getter/setter,还能携带注解。

我个人习惯用 KProperty 来做字段级别的注解处理。比如写一个自定义的 @BindView 注解,运行时通过反射拿到所有带这个注解的属性,然后自动赋值。

// 定义一个简单的类
data class User(val name: String, var age: Int)

fun inspectProperty() {
    val prop = User::name  // 获取KProperty
    println("属性名: ${prop.name}")        // name
    println("返回类型: ${prop.returnType}") // kotlin.String
    println("是否常量: ${prop.isConst}")    // false
    
    // 对于var属性,可以用KMutableProperty
    val ageProp = User::age as KMutableProperty<Int>
    val user = User("Alice", 25)
    ageProp.setter.call(user, 30)
    println(user.age) // 30
}

关键区别:

  • KProperty 对应 val(只读)
  • KMutableProperty 对应 var(可读写)
  • 通过 getter / setter 可以拿到对应的 KFunction

我的经验: 在Android的ViewModel中,我用KProperty配合LiveData做自动绑定。给属性加个 @Observable 注解,反射遍历所有属性,自动生成观察代码。省掉了手写一堆 observe 的重复劳动。

2.2 KFunction:函数的“遥控器”

KFunction 代表一个函数。你可以通过它动态调用函数、获取参数信息、甚至修改函数的访问权限。

我曾经在写一个AOP框架时,需要拦截所有带 @Log 注解的函数。用 KFunction 配合 call() 方法,在调用前后插入日志逻辑,比Java的 Method.invoke() 更安全——因为Kotlin的反射会保留类型信息。

fun greet(name: String, age: Int): String {
    return "Hello, $name! You are $age years old."
}

fun callFunction() {
    val func = ::greet  // 获取KFunction
    println("函数名: ${func.name}")        // greet
    println("参数数量: ${func.parameters.size}") // 2
    
    // 调用函数
    val result = func.call("Bob", 28)
    println(result) // Hello, Bob! You are 28 years old.
    
    // 更安全的调用方式:按参数名传值
    val result2 = func.callBy(mapOf(
        func.parameters[0] to "Charlie",
        func.parameters[1] to 35
    ))
    println(result2) // Hello, Charlie! You are 35 years old.
}

注意: call() 方法如果参数类型不匹配,会在运行时抛异常。我个人建议优先用 callBy(),它更安全,而且支持默认参数值。

2.3 KParameter:参数的“说明书”

KParameter 描述函数或构造函数的参数。它告诉你参数叫什么名字、是什么类型、是不是可选的(有没有默认值)。

为什么这个重要?你想想看,在写依赖注入框架时,你需要知道构造函数每个参数的类型,才能决定注入什么对象。我早期用Java反射时,参数名默认是 arg0arg1……那叫一个痛苦。Kotlin的 KParameter 直接保留了参数名,舒服多了。

class Person(val name: String, val age: Int = 18)

fun inspectParameters() {
    val constructor = ::Person  // 获取构造函数引用
    val params = constructor.parameters
    
    params.forEach { param ->
        println("参数名: ${param.name}")           // name / age
        println("参数类型: ${param.type}")         // kotlin.String / kotlin.Int
        println("是否可选: ${param.isOptional}")   // false / true
        println("参数索引: ${param.index}")        // 0 / 1
        println("---")
    }
    
    // 利用参数信息动态构造对象
    val person = constructor.callBy(mapOf(
        params[0] to "David",
        // params[1] 有默认值,可以不传
    ))
    println(person) // Person(name=David, age=18)
}

实用技巧: isOptional 属性非常有用。在写Builder模式或工厂模式时,你可以根据参数是否有默认值来决定是否必须提供该参数。我曾经用这个特性做了一个“智能Builder”,自动跳过有默认值的参数。

2.4 KType:类型的“放大镜”

KType 是Kotlin反射中最“高级”的概念。它不仅仅描述一个类型,还能描述泛型参数、可空性、类型投影等Kotlin特有的类型信息。

Java的 Type 接口处理泛型时非常麻烦,尤其是嵌套泛型。Kotlin的 KType 就清晰多了——它用 arguments 属性直接暴露泛型参数,用 isMarkedNullable 判断可空性。

fun <T> inspectType(list: List<T>) {
    val type = list::class.createType()  // 获取KType
    println("类型: ${type.classifier}")  // class kotlin.collections.List
    
    // 获取泛型参数
    val arg = type.arguments.first()
    println("泛型参数: ${arg.type}")     // 实际类型(取决于T)
    println("泛型方差: ${arg.variance}") // INVARIANT / IN / OUT
}

// 测试
inspectType(listOf("a", "b", "c"))
// 输出:
// 类型: class kotlin.collections.List
// 泛型参数: kotlin.String
// 泛型方差: INVARIANT

避坑指南: 我曾经在解析泛型时踩过一个坑——KTypeclassifier 可能返回 KClassTypeParameter。如果是泛型参数(比如 T),classifier 是一个 TypeParameter 实例,不是具体的类。记得用 is TypeParameter 做判断。

2.5 四个API的协作实战

光说理论没意思,咱们来个综合例子。假设你要写一个通用的“对象转Map”工具,把数据类的属性名和值提取出来:

fun <T : Any> toMap(obj: T): Map<String, Any?> {
    val kClass = obj::class
    val result = mutableMapOf<String, Any?>()
    
    // 遍历所有属性
    kClass.memberProperties.forEach { prop ->
        // prop 是 KProperty
        val value = prop.getter.call(obj)
        val type = prop.returnType  // KType
        
        // 如果属性是集合类型,递归处理
        if (type.classifier == List::class) {
            val list = value as? List<*>
            result[prop.name] = list?.map { 
                if (it != null && it::class.isData) toMap(it) else it 
            }
        } else {
            result[prop.name] = value
        }
    }
    
    return result
}

// 使用
data class Address(val city: String, val zip: String)
data class Employee(val name: String, val address: Address)

val emp = Employee("Eve", Address("Beijing", "100000"))
val map = toMap(emp)
println(map) 
// {name=Eve, address={city=Beijing, zip=100000}}

核心思路:

  • KClass.memberProperties 拿到所有属性(KProperty
  • prop.getter.call(obj) 获取属性值(KFunction
  • prop.returnType 判断类型(KType
  • 递归时用 KParameter 检查构造函数参数

你看,这四个API不是孤立的。在实际项目中,它们经常一起出现。我个人写注解处理器时,最常用的组合就是:KClassmemberPropertiesKPropertyreturnTypeKType,一条链走下来,信息全拿到了。

性能提醒: 反射有开销。在Android主线程频繁调用反射,可能会引起卡顿。我建议把反射结果缓存起来,比如用 ConcurrentHashMap 缓存 KClassKFunction 的引用。另外,ProGuard/R8 混淆时记得保留反射相关的类和成员。

好了,这一章的内容就到这里。四个API的用法你心里应该有数了。下一章咱们会深入注解处理器的实战——到时候这些反射知识全都会用上。


公众号:蓝海资料掘金营,微信deep3321