搞懂 kotlin 泛型 out 和 in 关键字

8 阅读2分钟

一、泛型的基本概念

泛型是一种编程机制,允许在定义类、接口或方法时使用类型参数。这样,在使用时可以指定具体的类型,从而实现代码的复用并提高类型安全性,避免了显式的类型转换和潜在的运行时错误。例如,List<String> 就明确了集合内只能存放字符串。

二、关键字out、in的使用

//out的使用
interface IContainer<out R>{
   fun get():R
}

//in的使用
interface IContainer<in V>{
   fun set(data:V)
}

三、泛型的进阶概念:型变 (Variance)、out (协变 - Covariant)、in (逆变 - Contravariant)

  • 不变 (Invariant) :泛型的默认行为,即使 A 是 B 的子类,Container<A> 与 Container<B> 之间也没有任何派生关系。
  • 型变 (## Variance) :是指泛型类型在基础类型发生变化时,其派生泛型类型如何跟随变化的性质。
  • 协变 (Covariant) :如果 A 是 B 的子类,那么 Container<A> 被视为 Container<B> 的子类,则称之为协变。在 Kotlin 中使用 out 关键字,表示该容器只能作为“生产者”输出数据。
  • 逆变 (Contravariant) :如果 A 是 B 的子类,那么 Container<B> 被视为 Container<A> 的子类,则称之为逆变。在 Kotlin 中使用 in 关键字,表示该容器只能作为“消费者”输入数据。

四、为什么需要out、in

举例说明:

interface Fruit // 定义“水果”类型
class Apple : Fruit // 定义“苹果”类型
class Banana : Fruit // 定义“香蕉”类型
class Container<T> // 定义容器

虽然 Apple 和 Banana 是Fruit 的子类,但是Container与Container、Container 之间没有任何关系,也不能相互赋值。

var fruitContainer: Container<Fruit> = Container<Fruit>()
var appleContainer: Container<Apple> = Container<Apple>()
val BananaContainer: Container<Banana> = Container<Banana>()

// 下面的写法都是错的,不能编译
fruitContainer = appleContainer
fruitContainer = BananaContainer
appleContainer = BananaContainer

当限制条件满足时,这种类型转换在逻辑上是符合直觉的,如下代码所示:

open class Fruit
class Apple : Fruit()
class Banana : Fruit()

abstract class Container<T> {
    abstract fun get(): T
}

var fruitContainer: Container<Fruit> = object : Container<Fruit>() {
    override fun get(): Fruit {
        return Fruit()
    }
}

var appleContainer: Container<Apple> = object : Container<Apple>() {
    override fun get(): Apple {
        return Apple()
    }
}

fruitContainer 预期的结果类型是 Fruit,而 appleContainer 只能提供 Apple,因为Apple 是 Fruit 的子类,从抽象逻辑上来看 fruitContainer = appleContainer 是可以的。但是编译器无法理解,因此需要显式地告诉编译器它们之间存在的关系。

使用 out 关键字定义协变关系(限定输出类型):

abstract class Container<out T> {
    abstract fun get(): T
}

var appleContainer: Container<Apple> = object : Container<Apple>() {
    override fun get(): Apple {
        return Apple()
    }
}
fruitContainer = appleContainer // 可以通过编译

使用 in 关键字定义逆变关系(限定输入的类型):

abstract class Container<in T> {
    abstract fun set(data: T)
}

var fruitContainer: Container<Fruit> = object : Container<Fruit>() {
    override fun set(data: Fruit) {
        TODO("Not yet implemented")
    }
}

// fruitContainer 是可以赋值给 appleContainer 的,可以通过编译
var appleContainer: Container<Apple> = fruitContainer
appleContainer.set(Apple())

五、核心意义

引入 out 和 in 关键字是为了打破泛型默认的“不变性”,在逻辑合理且类型安全的前提下,允许更灵活的类型转换与赋值。