3. KCallable与KFunction:函数调用、参数处理、可变参数与默认参数的反射调用
反射调用函数,说白了就是「在运行时才知道要调哪个方法」。这在写框架、做动态代理、搞依赖注入时特别常见。我个人习惯把KCallable和KFunction看作Kotlin反射体系里的「执行者」——它们负责把代码里的函数调用,变成运行时可以操控的对象。
3.1 KCallable:一切可调用成员的抽象
KCallable是所有可调用元素的父接口。函数、属性、构造方法,都实现了它。它提供了三个核心能力:
- call():最基础的调用方式,传参按声明顺序
- callBy():支持按参数名传参,还能处理默认值
- 参数信息:通过parameters属性拿到所有参数详情
我在项目中遇到过一个问题:用call()调用带默认参数的函数时,如果不传默认参数,会直接抛异常。嗯,这里要注意——call()不会自动填充默认值,你得自己处理。
核心区别:
- call():参数必须全部显式传入,顺序必须一致
- callBy():可以只传部分参数,其余用默认值
3.2 KFunction:函数的反射表示
KFunction继承自KCallable,专门表示函数。它多了一个isSuspend属性,用来判断是不是挂起函数。你想想看,在写通用调用框架时,这个属性有多重要——挂起函数的调用方式完全不同。
获取KFunction的方式很简单:
// 顶层函数
val func = ::sayHello // 返回 KFunction1<String, Unit>
// 成员函数
class MyClass {
fun greet(name: String) = "Hello, $name"
}
val greetFunc = MyClass::greet // 返回 KFunction2<MyClass, String, String>
// 扩展函数
fun String.addExclamation() = this + "!"
val extFunc = String::addExclamation // 返回 KFunction1<String, String>
注意看类型参数:KFunction1表示一个参数,KFunction2表示两个参数,以此类推。最后一个类型是返回值类型。这个命名规则,说白了就是Kotlin的「函数类型」在反射层面的映射。
3.3 参数处理:从parameters到KParameter
每个KCallable都有一个parameters属性,返回List<KParameter>。KParameter包含了:
| 属性 | 说明 | 我在项目中踩过的坑 |
|---|---|---|
| index | 参数在声明中的位置 | 扩展函数的receiver参数index是0 |
| name | 参数名(可能为null) | Java编译的字节码可能丢失参数名 |
| type | 参数的类型 | 泛型擦除后需要reified才能拿到真实类型 |
| kind | VALUE、EXTENSION_RECEIVER、INSTANCE | 成员函数的第一个参数是INSTANCE |
| isOptional | 是否有默认值 | callBy()依赖这个属性 |
| isVararg | 是否是可变参数 | 可变参数在反射中是个数组 |
我曾经在写一个参数校验框架时,忘了处理EXTENSION_RECEIVER类型的参数,结果扩展函数调用时总是少传一个参数。排查了半天才发现——receiver参数也是参数,不能跳过。
3.4 可变参数的反射调用
可变参数在反射中比较特殊。你想想看,Java的可变参数本质是个数组,Kotlin也一样。但Kotlin在反射层面做了包装:
fun printAll(vararg items: String) {
items.forEach { println(it) }
}
fun main() {
val func = ::printAll
// 方式一:直接传数组(不推荐)
func.call(arrayOf("a", "b", "c"))
// 方式二:用callBy按参数名传(推荐)
val params = func.parameters
val argMap = mapOf(
params[0] to arrayOf("a", "b", "c")
)
func.callBy(argMap)
}
注意:用call()传可变参数时,如果直接传数组,Kotlin会把它当作一个参数而不是展开。正确的做法是用callBy(),或者用*展开操作符。但反射里没有*操作符,所以callBy()是更安全的选择。
3.5 默认参数的反射调用
默认参数是Kotlin的一大特色。但在反射中,默认参数不会自动生效。我刚开始用反射时就被这个坑过——明明函数有默认值,call()却报参数数量不匹配。
解决方案有两个:
- 用callBy():只传需要覆盖的参数,其余用默认值
- 手动补全默认值:通过isOptional判断,然后从函数声明中获取默认值
fun greet(name: String, greeting: String = "Hello") {
println("$greeting, $name!")
}
fun main() {
val func = ::greet
val params = func.parameters
// 正确做法:只传name,greeting用默认值
val argMap = mapOf(
params[0] to "Kotlin" // name参数
// greeting参数不传,自动用默认值
)
func.callBy(argMap) // 输出:Hello, Kotlin!
// 错误做法:call()必须传全部参数
// func.call("Kotlin") // 报错:参数数量不匹配
}
个人经验:在写框架时,我建议统一用callBy()代替call()。虽然多写几行代码,但能避免很多运行时异常。特别是当函数签名可能变化时,callBy()的容错性更好。
3.6 实战:通用函数调用器
把上面的知识点串起来,写一个通用的函数调用器。这个工具我在写一个配置驱动的任务调度框架时用过:
class FunctionInvoker {
fun invoke(func: KCallable<*>, args: Map<String, Any?>): Any? {
val params = func.parameters
val argMap = mutableMapOf<KParameter, Any?>()
for (param in params) {
when (param.kind) {
KParameter.Kind.INSTANCE -> {
// 成员函数的receiver
val receiver = args["this"]
?: throw IllegalArgumentException("缺少receiver")
argMap[param] = receiver
}
KParameter.Kind.EXTENSION_RECEIVER -> {
// 扩展函数的receiver
val receiver = args["receiver"]
?: throw IllegalArgumentException("缺少扩展receiver")
argMap[param] = receiver
}
KParameter.Kind.VALUE -> {
val value = args[param.name]
if (value == null && !param.isOptional) {
throw IllegalArgumentException("缺少参数: ${param.name}")
}
if (value != null) {
argMap[param] = value
}
// isOptional为true且value为null时,不传参,使用默认值
}
}
}
return func.callBy(argMap)
}
}
// 使用示例
fun add(a: Int, b: Int = 10) = a + b
fun main() {
val invoker = FunctionInvoker()
val result = invoker.invoke(::add, mapOf("a" to 5))
println(result) // 15,b用了默认值10
}
这个调用器处理了三种情况:普通参数、receiver参数、默认参数。嗯,其实还可以加上类型转换和异常处理,但核心逻辑就是这样。
3.7 性能与注意事项
反射调用比直接调用慢,这是事实。我做过测试,在JVM上反射调用大约比直接调用慢10-20倍。但如果你只是做初始化时的配置解析,或者调用频率不高,这个性能损失完全可以接受。
几个避坑指南:
- 我曾经在循环里用反射调用,结果性能瓶颈全在这。后来改成缓存KFunction实例,性能提升明显。
- 我曾经没处理isSuspend属性,结果挂起函数在普通协程里调用,直接崩溃。记住:挂起函数要用KFunction的callSuspend()。
- 我曾经在Java互调时忘了参数名可能为null,结果用name做key时NPE。建议用index作为备选方案。
总结一下:KCallable和KFunction是Kotlin反射的执行核心。call()简单直接但功能有限,callBy()灵活但需要处理参数映射。可变参数和默认参数是反射调用中的两个常见陷阱,记住用callBy()就能避开大部分坑。
下一章我们会深入KProperty,看看属性的反射调用和委托属性的实现原理。到时候你会发现,属性的反射比函数更微妙——毕竟属性背后可能藏着getter/setter,甚至整个委托逻辑。