一句话总结:
out和in是Kotlin用来保证泛型类型安全的“交通规则”。out规定了“生产/输出”通道(协变),in规定了“消费/输入”通道(逆变),其根本目的是防止你在类型转换后,做出“把香蕉放进苹果箱”这样的危险操作。
一、问题的根源:List<苹果>是List<水果>吗?
在开始之前,我们先思考一个问题:如果苹果是水果的子类,那么一个装满苹果的列表List<苹果>,能否被当作一个List<水果>来使用?
直觉上是可以的,因为从里面拿出来的每个苹果“都是”水果。但如果这是一个可写的MutableList呢?
val appleList: MutableList<苹果> = mutableListOf(苹果())
val fruitList: MutableList<水果> = appleList // 编译错误!如果这行能通过...
fruitList.add(香蕉()) // ...那么我们就能把香蕉放进一个只能装苹果的列表里!
val apple: 苹果 = appleList[1] // ...从里面取出来时,就会发生类型转换异常!
为了在编译期就杜绝这种风险,Kotlin泛型默认是不变的 (Invariant) 。而out和in就是我们告诉编译器“在特定场景下可以安全地放宽限制”的方式。
二、型变的两种“玩法”:声明处 vs. 使用处
1. 声明处型变 (Declaration-site Variance): 给你的类预设角色
当你自己设计一个泛型类或接口时,可以预先声明它的型变特性。
-
out- 生产者 (Producer) :如果你的类只用于“生产”或“提供”T类型的值。// 水果箱只出不进,是天生的生产者 interface 水果箱<out T> { fun 拿出水果(): T // fun 放入水果(item: T) // 编译错误!T不能出现在输入位置 }因为编译器知道这个箱子是“只出不进”的,所以它允许我们安全地进行协变转换:
水果箱<苹果>可以被赋值给水果箱<水果>。 -
in- 消费者 (Consumer) :如果你的类只用于“消费”或“处理”T类型的值。// 垃圾桶只进不出,是天生的消费者 interface 垃圾桶<in T> { fun 扔垃圾(item: T) // fun 取出垃圾(): T // 编译错误!T不能出现在输出位置 }因为编译器知道这个垃圾桶是“只进不出”的,所以它允许我们安全地进行逆变转换:
垃圾桶<水果>可以被赋值给垃圾桶<香蕉皮>。(你可以用一个能装任何水果的垃圾桶,来专门装香蕉皮)。
2. 使用处型变 (Use-site Variance): 临时赋予角色
对于本身是不变的泛型类(比如MutableList<T>,因为它既能读又能写),我们可以在使用它的时候,临时“投影”成一个生产者或消费者。
fun copyFrom(source: List<out 水果>, destination: MutableList<in 水果>) {
for (fruit in source) {
destination.add(fruit)
}
}
val appleList: List<苹果> = listOf(苹果())
val anyFruitList: MutableList<水果> = mutableListOf()
// 在调用时,编译器允许:
// 1. 将`List<苹果>`当作`List<out 水果>` (生产者)
// 2. 将`MutableList<水果>`当作`MutableList<in 水果>` (消费者)
copyFrom(appleList, anyFruitList)
这里的out和in,就是在使用时向编译器做出的“临时承诺”,让不变的类型也能灵活地用于协变或逆变场景。
三、型变的指导原则:PECS
记住这个行业标准缩写,能帮你做出正确的设计:PECS - Producer-out, Consumer-in。
- 当你的泛型参数
T主要用于返回值(生产T),就用out。 - 当你的泛型参数
T主要用于函数参数(消费T),就用in。 - 如果既用于参数又用于返回值,那就保持不变(不加关键字)。
四、Kotlin标准库中的实战
List<out E>:List接口是只读的,它是一个天生的生产者,所以被声明为out协变。Comparable<in T>:Comparable接口用于消费一个T类型的对象来进行比较(compareTo(other: T)),所以被声明为in逆变。MutableList<E>:这个接口既有get(index: Int): E(生产)又有add(element: E)(消费),所以它必须是不变的。
结论
in和out远不止是“只读/只写”的语法糖,它们是Kotlin类型系统中实现编译期安全与使用灵活性之间精妙平衡的核心机制。理解它们背后的“生产者/消费者”模型和两种型变场景,能让你在设计和使用泛型时,写出既灵活又绝对安全的代码,真正掌握这门语言的精髓。