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 协变与逆变

这是泛型里最绕的部分。我尽量用大白话讲清楚。

先看一个经典问题:如果 AppleFruit 的子类,那 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,也会出问题。

记住口诀:out 表示只读(生产者),in 表示只写(消费者)。Kotlin 标准库里的 List 是协变的,MutableList 不是。这就是为什么你可以把 List<Apple> 赋值给 List<Fruit>,但 MutableList 不行。

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 泛型知识体系 泛型 (Generics) 泛型类 泛型函数 类型约束 变型 (Variance) 协变 (out) 逆变 (in) 星投影 (*) 单约束 : Type 多约束 where 口诀:out 只读(生产者),in 只写(消费者),* 表示未知类型

泛型是 Kotlin 类型系统的核心能力。我个人觉得,掌握泛型的关键不在于记住所有语法,而在于理解「类型安全」这四个字。每次用泛型时问自己:这个类型参数是生产还是消费?需不需要约束?用协变还是逆变?想清楚这些问题,代码自然就稳了。

好了,泛型这部分就聊到这儿。记住多用、多练,遇到类型不匹配的错误时别慌,仔细分析一下是协变还是逆变的问题。嗯,你肯定能搞定。