理解 Kotlin 的协变和逆变

协变和逆变

  • 协变和逆变的本质是在处理”类型替换的安全性保证”这个问题

  • 这是类型系统用来保证类型安全的机制

    • 本质上是在处理”什么样的类型替换是安全的”这个问题
    • 协变和逆变提供了明确的规则来判断类型替换的安全性

协变 (out)

协变(out)通过限制类型参数只能用在输出位置,从而保证了类型安全
  • 协变关注的是”类型替换的安全性”
  • 协变保证了”子类型可以替换父类型”的里氏替换原则
  • 安全性来自于”子类型包含父类型所有行为”的保证,如果需要 Animal 的地方,提供一个 Cat 是安全的(因为 Cat 一定具备 Animal 的所有特性)
  • 这种保证是单向的:子类型可以安全地替换父类型,但反过来不行
// 安全的场景(协变)
val animals: Producer<Animal> = CatProducer()  // ✅ 安全
// 因为需要动物的地方,给一只猫一定可以满足需求

// 不安全的场景
val cats: Producer<Cat> = AnimalProducer()     // ❌ 不安全
// 因为需要猫的地方,给一个动物不能保证它一定是猫(反过来不行)
  • 当类型参数用在输入位置时,允许子类型替换父类型是不安全的,因为实现可能无法处理父类型的所有可能情况
interface AnimalShelter<T> {
    fun get() = T
    fun acceptAnimal(animal: T) // 收容动物
}

val catShelter: AnimalShelter<Cat> = CatShelter()  //专门处理猫
// 如果允许 catShelter 当作 AnimalShelter<Animal> 使用
// 那么别人可能会试图让猫舍收容狗,这显然是不对的

fun processcatShelter(animalShelter: AnimalShelter<Animal>) {
  val cat = animalShelter.get()
  val dog = Dog()
  animalShelter.acceptAnimal(animal)  // 如果这里使用的是Cat就是安全的,但是如果是其他就不安全,所以会报错!
  //❌错误点:调用者认为可以传入任何 Animal,但实际实现传入的参数只能处理 Cat,这导致了类型安全的破坏
  //如果能够确保运行时类型安全 可以在使用out修饰后用 @UnsafeVariance 注解,建议只是用来做只读操作比如:比较
}

processcatShelter(catShelter) //提供Cat做处理
  • 当类型参数只用在输出位置时,子类型替换父类型是安全的,因为子类型的对象一定满足父类型的要求
interface AnimalProvider<out T> {
    fun get(): T  // 只提供动物
}

val catProvider: AnimalProvider<Cat> = CatProvider()
// 允许 catProvider 当作 AnimalProvider<Animal> 使用
// 因为猫舍提供的猫一定是动物,这是安全的

逆变(in)

逆变(in)通过限制类型参数只能用在输入位置,从而保证了类型安全
  • 逆变关注的是”处理能力的包含关系”
  • 逆变保证了”更一般的处理器可以替换更具体的处理器”,因为子类型必定包含父类型的所有行为,如果需要处理 Cat 的代码,用处理 Animal 的代码也是安全的(因为处理 Animal 的代码不会用到 Cat 特有的特性)
  • 安全性来自于”能处理父类型就一定能处理子类型”
  • 这种保证也是单向的:处理父类型的代码可以安全地替换处理子类型的代码,但反过来不行
// 安全的场景(逆变)
val catHandler: Consumer<Cat> = AnimalHandler()  // ✅ 安全
// 因为能处理所有动物的代码,一定能处理猫

// 不安全的场景
val animalHandler: Consumer<Animal> = CatHandler()  // ❌ 不安全
// 因为只能处理猫的代码,不一定能处理其他动物(反过来不行)
  • 当类型参数用在输出位置时,允许父类型替换子类型是不安全的,因为实现可能返回父类型的任何对象
interface AnimalClinic<T> {
    fun getPatient(): T  // 获取病患
}

val catClinic: AnimalClinic<Cat> = AnimalClinic()
// 如果允许 AnimalClinic<Animal> 当作 AnimalClinic<Cat> 使用
// 那么可能会返回一只狗给期望获得猫的代码,这显然是不对的
  • 当类型参数只用在输入位置时,父类型替换子类型是安全的,因为能处理父类型就一定能处理子类型
interface AnimalTreatment<in T> {
    fun treat(patient: T)  // 只处理病患
}

val animalTreatment: AnimalTreatment<Animal> = AnimalTreatment()
// 允许 animalTreatment 当作 AnimalTreatment<Cat> 使用
// 因为能治疗所有动物的医生一定能治疗猫,这是安全的

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!