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()却报参数数量不匹配。

解决方案有两个:

  1. 用callBy():只传需要覆盖的参数,其余用默认值
  2. 手动补全默认值:通过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,甚至整个委托逻辑。