1. Kotlin协变与逆变深度解析
在Kotlin泛型系统中,协变和逆变是两个至关重要的高级特性。它们解决了泛型类型之间的子类型关系问题,使得我们的代码既能保持类型安全,又能具备足够的灵活性。作为一名长期使用Kotlin进行开发的工程师,我发现很多开发者对这些概念的理解存在误区,今天我就来详细剖析它们的本质和应用场景。
1.1 为什么需要协变和逆变?
让我们从一个实际开发中常见的痛点开始。假设我们有以下类层次结构:
kotlin复制open class Animal
class Cat : Animal()
class Dog : Animal()
现在,我们有一个List<Cat>,直觉上我们认为它应该可以赋值给List<Animal>,因为Cat是Animal的子类。但实际情况是:
kotlin复制val catList: List<Cat> = listOf(Cat())
val animalList: List<Animal> = catList // 编译错误!
这个编译错误揭示了泛型的一个重要特性:默认情况下,泛型是**不变(Invariant)**的。也就是说,即使Cat是Animal的子类,List
1.2 核心概念解析
在深入探讨之前,我们需要明确几个关键术语:
| 概念 | 关键字 | 定义 | 实际意义 |
|---|---|---|---|
| 协变 | out | 如果A是B的子类,那么Generic也是Generic的子类 | 生产者角色,只能输出数据 |
| 逆变 | in | 如果A是B的子类,那么Generic是Generic的子类 | 消费者角色,只能输入数据 |
| 不变 | 无 | Generic和Generic之间没有子类关系 | 既能读取也能写入数据 |
记忆口诀:out是生产者(只能读),in是消费者(只能写)。这个简单的规则将贯穿我们整个讨论。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 协变(Covariance)深入剖析
2.1 协变的基本用法
协变通过out关键字实现,它允许我们将子类的泛型容器赋值给父类的泛型容器。让我们看一个自定义协变接口的例子:
kotlin复制interface Producer<out T> {
fun produce(): T
// 注意:这里不能有以T作为参数的方法
// fun consume(t: T) // 编译错误!
}
val catProducer: Producer<Cat> = object : Producer<Cat> {
override fun produce(): Cat = Cat()
}
val animalProducer: Producer<Animal> = catProducer // 协变允许这种赋值
这里的关键点是:Producer接口中的泛型类型T只能出现在返回位置,不能作为参数类型。这确保了类型安全。
2.2 标准库中的协变实例
Kotlin标准库中的List接口就是一个典型的协变例子:
kotlin复制public interface List<out E> : Collection<E> {
// 只有读取操作
public operator fun get(index: Int): E
// 没有添加元素的方法
}
正因为List是只读的,它才能安全地声明为协变。这使得我们可以这样做:
kotlin复制fun printAnimals(animals: List<Animal>) {
animals.forEach { println(it) }
}
val cats: List<Cat> = listOf(Cat(), Cat())
printAnimals(cats) // 完全合法
2.3 协变的安全性保证
协变之所以安全,是因为它遵循了一个重要原则:只能输出,不能输入。这意味着:
- 我们可以安全地从`Producer<An
