2、泛型函数:定义泛型函数、类型参数推断、多个类型参数

好,咱们接着聊泛型。上一章我们讲了泛型类和泛型接口,说白了就是给「类型」留个坑位。那泛型函数呢?其实思路一样——给函数的参数或返回值留个类型坑位。

我个人习惯把泛型函数叫做「万能函数模板」。你想想看,有时候我们写一个函数,逻辑完全一样,只是参数类型不同。比如交换两个变量的值,Int 要写一个,String 又要写一个?太累了。泛型函数就是来解决这个问题的。

2.1 定义泛型函数

定义泛型函数很简单,在函数名前面加一对尖括号,里面放类型参数。语法长这样:

fun <T> swap(a: T, b: T): Pair<T, T> {
    return Pair(b, a)
}

这里的 <T> 就是类型参数声明。它告诉编译器:「嘿,这个 T 是个占位符,具体是什么类型,调用的时候再说。」

我在项目中遇到过这样一个场景:需要把任意类型的列表转成 Map,key 是元素本身,value 是元素的一个属性。用泛型函数就特别顺手:

fun <T, R> List<T>.toMapBy(selector: (T) -> R): Map<R, T> {
    val result = mutableMapOf<R, T>()
    for (item in this) {
        result[selector(item)] = item
    }
    return result
}

嗯,这里要注意:泛型函数的类型参数要放在 fun 关键字和函数名之间。位置错了,编译器会报错。

核心要点:泛型函数的类型参数声明必须紧跟在 fun 关键字之后,函数名之前。这是 Kotlin 的语法规则,没得商量。

2.2 类型参数推断

Kotlin 有一个很贴心的特性——类型参数推断。说白了就是:你不需要每次都手动指定类型,编译器能自己猜出来。

举个例子:

val result = swap(10, 20)      // 编译器推断 T 为 Int
val result2 = swap("A", "B")   // 编译器推断 T 为 String

你看,我根本没写 <Int> 或者 <String>,编译器自己就搞定了。为什么会这样?因为编译器会根据传入的参数类型自动推导出 T 的具体类型。

不过,有时候编译器也会「犯迷糊」。比如这种情况:

fun <T> createList(): List<T> = emptyList()

// 这样写会报错——编译器不知道 T 是什么
val list = createList()

// 需要显式指定类型
val list: List<String> = createList()
val list2 = createList<String>()

我曾经踩过这个坑。写了一个泛型工厂函数,返回值类型完全由类型参数决定。结果调用的时候忘了指定类型,编译器一脸懵。从那以后,我养成了一个习惯:如果函数的返回值类型完全依赖类型参数,调用时要么显式声明变量类型,要么显式指定类型参数。

小技巧:当编译器无法推断类型参数时,可以用两种方式帮它一把:1) 显式指定变量类型;2) 在函数名后加尖括号指定类型参数。我个人更推荐第一种,代码更干净。

2.3 多个类型参数

一个泛型函数可以有多个类型参数。比如我们经常用的 Map,就有 K 和 V 两个类型参数。函数也一样:

fun <K, V> putIfAbsent(map: MutableMap<K, V>, key: K, value: V): V? {
    return if (map.containsKey(key)) {
        map[key]
    } else {
        map[key] = value
        null
    }
}

多个类型参数用逗号分隔。调用的时候,Kotlin 同样会尝试推断:

val map = mutableMapOf<String, Int>()
putIfAbsent(map, "age", 25)  // 编译器推断 K=String, V=Int

这里有个有意思的点:类型参数的顺序很重要。按照惯例,我们通常把更常用的类型参数放在前面。比如 Map<K, V>,K 是 key,V 是 value,这个顺序大家都习惯了。

再来看一个更复杂的例子——把两个列表「拉链」合并:

fun <T, R> zipToList(list1: List<T>, list2: List<R>): List<Pair<T, R>> {
    val result = mutableListOf<Pair<T, R>>()
    val minSize = minOf(list1.size, list2.size)
    for (i in 0 until minSize) {
        result.add(Pair(list1[i], list2[i]))
    }
    return result
}

这个函数有两个类型参数 T 和 R,分别对应两个列表的元素类型。调用时:

val names = listOf("Alice", "Bob", "Charlie")
val scores = listOf(95, 87, 92)
val zipped = zipToList(names, scores)  // 类型推断:T=String, R=Int

避坑指南:我曾经在项目里写过五个类型参数的泛型函数,结果代码可读性极差,同事看了直摇头。我的建议是:类型参数不要超过三个。如果超过三个,说明你的函数职责可能太杂了,考虑拆分成多个小函数。

2.4 实战:泛型函数的最佳实践

说了这么多,咱们来点实际的。我在写工具库时经常用泛型函数封装一些通用逻辑。比如这个「安全类型转换」函数:

inline fun <reified T> safeCast(obj: Any?): T? {
    return if (obj is T) obj else null
}

注意这里用了 inlinereified 关键字。reified 是 Kotlin 的一个黑科技,它让类型参数在运行时「活」过来了。普通泛型在运行时类型信息会被擦除,但 reified 不会。这个我们后面会专门讲。

再比如,我经常需要把任意类型的值转成 JSON 字符串:

fun <T> toJsonString(value: T): String {
    return when (value) {
        is String -> "\"$value\""
        is Number -> value.toString()
        is Boolean -> value.toString()
        is List<*> -> value.joinToString(prefix = "[", postfix = "]") { toJsonString(it) }
        is Map<*, *> -> value.entries.joinToString(prefix = "{", postfix = "}") { 
            "\"${it.key}\": ${toJsonString(it.value)}"
        }
        else -> "\"$value\""
    }
}

这个函数只有一个类型参数 T,但通过 when 表达式处理了多种情况。你看,泛型函数不一定要很复杂,关键是解决实际问题。

场景 类型参数数量 推荐写法
单一类型操作(如交换、转换) 1个 fun <T> ...
两种类型关联(如键值对、映射) 2个 fun <K, V> ...
复杂组合(如多列表合并) 2-3个 fun <T, R, S> ...

最后说一句:泛型函数是 Kotlin 泛型的基石。你把它玩明白了,后面学泛型约束、逆变协变就会轻松很多。下一章我们聊聊泛型约束——怎么限制类型参数的范围,让它既灵活又安全。