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
}
注意这里用了 inline 和 reified 关键字。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 泛型的基石。你把它玩明白了,后面学泛型约束、逆变协变就会轻松很多。下一章我们聊聊泛型约束——怎么限制类型参数的范围,让它既灵活又安全。