搞懂 kotlin 泛型 out 和 in 关键字

一、泛型的基本概念

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

二、关键字out、in的使用

kotlin 复制代码
//out的使用
interface IContainer<out R>{
   fun get():R
}

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

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

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

四、为什么需要out、in

举例说明:

kotlin 复制代码
interface Fruit // 定义"水果"类型
class Apple : Fruit // 定义"苹果"类型
class Banana : Fruit // 定义"香蕉"类型
class Container<T> // 定义容器

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

swift 复制代码
var fruitContainer: Container<Fruit> = Container<Fruit>()
var appleContainer: Container<Apple> = Container<Apple>()
val BananaContainer: Container<Banana> = Container<Banana>()

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

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

kotlin 复制代码
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 关键字定义协变关系(限定输出类型):

kotlin 复制代码
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 关键字定义逆变关系(限定输入的类型):

kotlin 复制代码
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())

五、核心意义

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

相关推荐
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Android的“旧时光”书店App为例,包含答辩的问题和答案
android
hashiqimiya2 小时前
androidstudio历史版本
android
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Android的出租车运行监测系统设计与实现为例,包含答辩的问题和答案
android
fatiaozhang95272 小时前
晶晨S905W2芯片_sbx_x98_plus_broagcon_atv_安卓11_线刷包固件包
android·电视盒子·刷机固件·机顶盒刷机·机顶盒刷机固件大全·晶晨s905w2芯片
匆忙拥挤repeat2 小时前
Android Compose 依赖配置解读
android
没有了遇见2 小时前
Android 关于注入Js处理Android和H5 Js 交互问题
android
阿拉斯攀登2 小时前
第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例
android·驱动开发·rk3568·瑞芯微·嵌入式驱动·安卓驱动·spi 设备驱动
Predestination王瀞潞2 小时前
Mysql忘记密码重置的方法
android·mysql·adb
闻哥2 小时前
MySQL三大日志深度解析:redo log、undo log、binlog 原理与实战
android·java·jvm·数据库·mysql·adb·面试