泛型基础:泛型类、泛型函数、类型参数约束、泛型变体
泛型这东西,说白了就是「类型上的参数化」。我刚开始学 Kotlin 时,觉得泛型不就是 Java 那套吗?后来踩了几个坑才发现,Kotlin 的泛型设计要优雅得多,也严谨得多。今天咱们就把这块啃透。
一、泛型类:让类型成为可配置的参数
先看一个最简单的例子。假设我们要做一个盒子,能装任何类型的东西。没有泛型时,你只能写 Any,然后到处做类型转换。有了泛型,事情就简单了:
class Box<T>(val item: T)
fun main() {
val stringBox = Box("Hello")
val intBox = Box(42)
// 类型安全,不用强转
val str: String = stringBox.item
}
这里 T 就是类型参数。你实例化时传什么类型,item 就是什么类型。我个人习惯把泛型类想象成一个「类型模具」——你往里注入具体类型,它就产出对应的具体类。
Box("Hello") 不用写成 Box<String>("Hello"),省点键盘寿命。
二、泛型函数:函数级别的类型抽象
有时候我们不需要整个类都泛型化,只需要某个函数能处理多种类型。这时候就用泛型函数:
fun <T> printItem(item: T) {
println("Item: $item")
}
// 调用时自动推断类型
printItem("Kotlin")
printItem(2024)
你想想看,如果不用泛型,你得为每种类型重载一个函数,那代码得多臃肿。我在项目中经常用泛型函数来做数据转换,比如把列表里的元素统一映射成另一种类型:
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) {
result.add(transform(item))
}
return result
}
嗯,这里要注意,泛型函数的类型参数要放在函数名之前,用尖括号括起来。这是 Kotlin 的语法规则,别写反了。
三、类型参数约束:给泛型加上边界
泛型太自由了也不好。有时候我们希望 T 必须是某个类型的子类,或者必须实现某个接口。这时候就用 where 子句来约束:
fun <T : Comparable<T>> maxOf(a: T, b: T): T {
return if (a >= b) a else b
}
// 多个约束
fun <T> process(item: T) where T : CharSequence, T : Comparable<T> {
println("Length: ${item.length}")
}
我曾经在项目中犯过一个低级错误:写了一个泛型函数,想对任意类型做排序,结果传进来一个不支持比较的对象,运行时直接崩溃。从那以后,我只要涉及比较操作,一定会加上 Comparable 约束。
T : SomeClass 表示 T 必须是 SomeClass 的子类(或实现类)。如果没有指定上界,默认上界是 Any?。如果你不希望类型可空,可以显式写成 T : Any。
四、泛型变体:协变、逆变、不变
这是泛型里最绕的部分,但也是 Kotlin 做得比 Java 好的地方。咱们先看一个经典问题:
open class Animal
class Dog : Animal()
// 假设有这样的函数
fun feed(animals: List<Animal>) {}
val dogs: List<Dog> = listOf(Dog())
feed(dogs) // 这行能编译吗?
答案是:能。因为 List<T> 在 Kotlin 中是协变的。但如果是 MutableList<T>,就不行了。为什么会这样?
咱们用表格来理清这三种变体:
| 变体 | 关键字 | 含义 | 典型例子 |
|---|---|---|---|
| 协变 | out |
生产者:只能读取,不能写入 | List<out T> |
| 逆变 | in |
消费者:只能写入,不能读取 | Comparable<in T> |
| 不变 | 无 | 既读又写,类型必须严格匹配 | MutableList<T> |
说白了,out 表示「我只会产出 T,不会消费 T」,所以你可以把 List<Dog> 当成 List<Animal> 用。而 in 表示「我只会消费 T,不会产出 T」,所以你可以把 Comparable<Any> 当成 Comparable<String> 用。
我在项目中遇到过这样一个场景:定义了一个事件处理器接口,需要处理不同类型的事件。用逆变就特别合适:
interface EventHandler<in T : Event> {
fun handle(event: T)
}
class AppEventHandler : EventHandler<AppEvent> {
override fun handle(event: AppEvent) { /* ... */ }
}
// 可以把 AppEventHandler 赋值给 EventHandler<Event>
val handler: EventHandler<Event> = AppEventHandler()
你想想看,如果不用逆变,这里就得写一堆类型转换代码,又丑又不安全。
out= 输出 = 只读 = 协变in= 输入 = 只写 = 逆变- 无标注 = 读写 = 不变
五、知识体系总览
下面这张图把泛型的核心知识点串起来了,建议你保存下来当速查表:
六、实战中的一些体会
泛型这东西,光看理论容易晕。我建议你从两个角度去理解:
- 从使用者的角度:泛型让你写更少的类型转换代码,编译器帮你做类型检查。
- 从设计者的角度:泛型让你写出更通用的组件,一次编写,多处复用。
最后分享一个我自己的习惯:在定义泛型类或函数时,先问自己三个问题——这个类型参数是只读的吗?是只写的吗?还是既读又写?想清楚这个,变体标注就不会搞错。
公众号:蓝海资料掘金营,微信deep3321