声明处型变:in 与 out
正确使用协变(out)和逆变(in)类型参数。
声明处型变:in 与 out 是 CoddyKit 上的免费 Kotlin Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Kotlin Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Kotlin Academy 课程共包含 4 节课。
型变问题
在 Kotlin 中,List<String> 是 List<Any> 的子类型,因为 List 使用 out T 声明。如果没有型变注解,这种关系就不会成立。
val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }使用 out 实现协变
out T 表示该类只能生成 T 值(将其返回),不能消费这些值。因此,Producer<Dog> 是 Producer<Animal> 的子类型。
interface Producer<out T> {
fun produce(): T
}
class DogProducer : Producer<String> {
override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
val dog = DogProducer()
sound(dog) // OK: Producer<String> is a subtype of Producer<Any>
}使用 in 实现逆变
in T 表示该类只能消费 T 值(将其作为参数接收),不能生成这些值。因此,Consumer<Animal> 是 Consumer<Dog> 的子类型。
interface Consumer<in T> {
fun consume(item: T)
}
class Printer : Consumer<Any> {
override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
val printer = Printer()
feedDog(printer) // OK: Consumer<Any> is a subtype of Consumer<String>
}out 的限制:不能出现在输入位置
使用 out T 时,T 不能出现在参数(输入)位置。编译器会强制执行这一限制。
interface ReadOnly<out T> {
fun get(): T // OK: out position
// fun set(t: T) {} // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
override fun get() = value
}
fun main() {
val box: ReadOnly<Any> = Box<String>("hello")
println(box.get())
}in 的限制:不能出现在输出位置
使用 in T 时,T 不能出现在返回值(输出)位置。
interface WriteOnly<in T> {
fun set(t: T) // OK: in position
// fun get(): T {} // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
override fun set(t: T) = println("Received: $t")
}
fun main() {
val sink: WriteOnly<String> = Sink<Any>()
sink.set("hello")
}Kotlin 标准库示例
Comparable<in T> 是逆变的:Comparable<Number> 可以比较 Number 的任意子类型。List<out T> 是协变的:猫的列表也是动物的列表。
fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
val ints: List<Int> = listOf(3, 1, 2)
val sorted = sortNumbers(ints) // OK: List<Int> is List<Number>
println(sorted)
}不变性:MutableList
MutableList<T> 是不变的:它既能生成也能消费 T,因此不同 T 类型之间不存在子类型关系。
fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
val ints = mutableListOf<Int>(1, 2)
// addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
val nums = mutableListOf<Number>(1, 2)
addNumber(nums) // OK
println(nums)
}使用位置型变作为替代方案
当您无法修改类时,可以使用使用位置型变:在调用位置使用 out,将类型投影为协变类型。
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
for (item in from) to.add(item)
}
fun main() {
val src = mutableListOf("a", "b", "c")
val dest = mutableListOf<Any>()
copy(src, dest)
println(dest)
}实践:Repository 模式
对只读存储库使用协变,对只写接收端使用逆变,从而清晰地表示数据流。
interface Repository<out T> {
fun findAll(): List<T>
fun findById(id: Int): T?
}
interface Writer<in T> {
fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>型变选择指南
根据 T 的使用方式选择型变:out = 生产者(只返回 T),in = 消费者(只接收 T),不变 = 两者兼具(MutableList、Channel)。
// Quick mental model:
// out T: source/producer — List, Flow, Sequence
// in T: sink/consumer — Comparable, Continuation
// invariant: read+write — MutableList, Channel, MutableStateFlow
fun main() {
val nums: List<Number> = listOf(1, 2, 3) // List is out
println(nums)
}带 out 函数的不变类
即使类是不变的,也可以在使用位置为特定的函数参数使用 out。
class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
fun push(item: T) = items.add(item)
fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
// can only read, not write
println(stack.pop())
}
fun main() {
val s = Stack<String>()
s.push("hello")
printAll(s)
}快速检查
类型参数上的 out T 表示什么?
回顾
out T(协变)允许在生成值时建立子类型关系。in T(逆变)允许在消费值时建立子类型关系。不变类型(同时进行读写)在不同 T 之间没有子类型关系。
常见问题解答
「声明处型变:in 与 out」课时是免费的吗?
是的 — 「声明处型变:in 与 out」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Kotlin Academy 课程的其余内容,请升级到 CoddyKit PRO。 Kotlin Academy 课程共包含 4 节课。
「声明处型变:in 与 out」这节课中我会学到什么?
正确使用协变(out)和逆变(in)类型参数。 你通过在浏览器中直接运行的动手代码来练习 Kotlin Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Kotlin Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Kotlin Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「声明处型变:in 与 out」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Kotlin Academy 课中编写并运行代码吗?
能。每节 Kotlin Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 where 的泛型函数与类型约束
- 声明处型变:in 与 out
- 星号投影与 * 的使用时机
- 类型擦除与 reified 类型参数