13、泛型:泛型类与泛型函数、类型约束、协变与逆变、星投影
泛型这东西,刚接触时觉得有点绕。说白了,它就是让我们的代码能处理多种类型,而不是为每种类型都写一遍重复逻辑。我在项目中重构过一个数据解析模块,如果没有泛型,那代码量至少翻三倍。今天咱们就把泛型这块硬骨头啃下来。
13.1 泛型类与泛型函数
先看泛型类。你想想看,如果我要写一个盒子,既能装苹果也能装书本,最笨的办法就是写两个类。但有了泛型,一个类就够了。
class Box<T>(val item: T) {
fun getItem(): T = item
}
// 使用
val appleBox = Box<String>("红富士")
val intBox = Box<Int>(42)
我个人习惯在类名后面用单个大写字母表示泛型参数,T、E、K、V 都是常见选择。Kotlin 的类型推断很聪明,很多时候你甚至不用写尖括号:
val box = Box("自动推断类型") // 编译器知道这是 Box<String>
泛型函数也是类似思路。函数前面声明泛型参数,调用时自动推断:
fun <T> printItem(item: T) {
println("内容是:$item")
}
printItem("Hello") // T 被推断为 String
printItem(123) // T 被推断为 Int
我在项目中遇到过最实用的场景是写一个通用的网络请求封装。请求体、响应体都是泛型,一套代码处理所有 API 接口。嗯,那感觉确实很爽。
13.2 类型约束
泛型太自由了也不好。有时候你希望 T 必须是某个类的子类,或者必须实现某个接口。这时候就需要类型约束。
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
// 只能传 Number 及其子类
sum(1, 2) // OK
sum(1.5, 2.5) // OK
// sum("a", "b") // 编译错误!
多个约束用 where 关键字:
fun <T> printIfComparable(item: T)
where T : Comparable<T>, T : CharSequence {
println("可比较的字符串:$item")
}
13.3 协变与逆变
这是泛型里最绕的部分。我尽量用大白话讲清楚。
先看一个经典问题:如果 Apple 是 Fruit 的子类,那 Box<Apple> 是不是 Box<Fruit> 的子类?
答案:默认不是。但 Kotlin 给了我们两个选择——协变和逆变。
协变(out):生产者。只能读取,不能写入。
class Producer<out T>(private val item: T) {
fun produce(): T = item
// fun consume(t: T) {} // 编译错误!out 位置不能作为参数
}
val appleProducer: Producer<Apple> = Producer(Apple())
val fruitProducer: Producer<Fruit> = appleProducer // 协变:子类转父类
逆变(in):消费者。只能写入,不能读取。
class Consumer<in T> {
fun consume(item: T) { /* 处理 */ }
// fun produce(): T { ... } // 编译错误!in 位置不能作为返回值
}
val fruitConsumer: Consumer<Fruit> = Consumer()
val appleConsumer: Consumer<Apple> = fruitConsumer // 逆变:父类转子类
为什么这样设计?你想想看,如果 Producer<out T> 允许写入,那 fruitProducer 里可能混入一个 Banana,但实际存的是 Apple,类型就乱了。反过来,Consumer<in T> 如果允许读取,读出来的可能是 Fruit,但调用方期望的是 Apple,也会出问题。
13.4 星投影
星投影用 * 表示,意思是「我不知道具体类型,但我知道它是某种类型」。
fun printList(list: List<*>) {
for (item in list) {
println(item)
}
}
printList(listOf(1, 2, 3))
printList(listOf("a", "b", "c"))
星投影的规则其实很严谨:
- 对于
Foo<out T>,Foo<*>等价于Foo<out Any?> - 对于
Foo<in T>,Foo<*>等价于Foo<in Nothing> - 对于
Foo<T>(不变),Foo<*>读取时是Any?,写入时不允许
我在项目中用星投影处理过异构数据列表。比如从数据库查出来的结果集,每行字段类型不确定,用 List<*> 接收最合适。但要注意,星投影只能读,不能写(除非是 Nothing 类型)。
* 时,你放弃了类型信息,编译器会限制你的操作。13.5 知识体系总览
下面这张图把泛型的核心概念串起来了,建议多看几遍,理解每个概念之间的关系:
泛型是 Kotlin 类型系统的核心能力。我个人觉得,掌握泛型的关键不在于记住所有语法,而在于理解「类型安全」这四个字。每次用泛型时问自己:这个类型参数是生产还是消费?需不需要约束?用协变还是逆变?想清楚这些问题,代码自然就稳了。
好了,泛型这部分就聊到这儿。记住多用、多练,遇到类型不匹配的错误时别慌,仔细分析一下是协变还是逆变的问题。嗯,你肯定能搞定。