Kotlin 2.1.0 入门教程(二十三)泛型、泛型约束、协变、逆变、不变

out(协变)

out 关键字用于实现泛型的协变。协变意味着如果 BA 的子类型,那么 Producer<B> 可以被视为 Producer<A> 的子类型。这里的 Producer 是一个使用泛型类型参数的类或接口,并且该泛型类型参数被标记为 out

kotlin 复制代码
interface Producer<out T> {
    fun produce(): T
}

open class Animal

class Cat : Animal()

fun main() {
    val catProducer: Producer<Cat> = object : Producer<Cat> {
        override fun produce(): Cat = Cat()
    }

    // 协变,允许这样的赋值。
    val animalProducer: Producer<Animal> = catProducer 
}

在这个例子中,由于 Producer 接口的泛型参数 T 被标记为 out,所以 Producer<Cat> 类型的对象可以赋值给 Producer<Animal> 类型的变量,因为 CatAnimal 的子类型。

in(逆变)

in 关键字用于实现泛型的逆变。逆变表示如果 BA 的子类型,那么 Consumer<A> 可以被视为 Consumer<B> 的子类型。这里的 Consumer 是一个使用泛型类型参数的类或接口,并且该泛型类型参数被标记为 in

kotlin 复制代码
interface Consumer<in T> {
    fun consume(item: T)
}

open class Animal

class Cat : Animal()

fun main() {
    val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
        override fun consume(item: Animal) {
        }
    }
    
    // 逆变,允许这样的赋值。
    val catConsumer: Consumer<Cat> = animalConsumer 
}

在这个例子中,因为 Consumer 接口的泛型参数 T 被标记为 in,所以 Consumer<Animal> 类型的对象可以赋值给 Consumer<Cat> 类型的变量,尽管 CatAnimal 的子类型。

where(泛型约束)

where 关键字用于为泛型类型参数添加额外的约束条件。它允许你指定泛型类型参数必须满足的多个条件。

kotlin 复制代码
class Container<T> where T : Number, T : Comparable<T> {
    private val items = mutableListOf<T>()

    fun add(item: T) {
        items.add(item)
    }

    fun getMax(): T? {
        return items.maxByOrNull { it }
    }
}

fun main() {
    val container = Container<Int>()
    container.add(10)
    container.add(20)
    println(container.getMax()) 
}

在这个例子中,Container 类的泛型类型参数 T 受到 where 子句的约束,要求 T 必须是 Number 类型,同时还必须实现 Comparable<T> 接口。这样,Container 类内部就可以安全地使用 Number 相关的操作以及 Comparable 接口提供的比较功能。

声明处型变:out(协变)

当一个类型参数 T 被声明为 out 时,意味着该类型参数只能在类或接口的成员中作为返回类型出现,而不能作为参数类型。这样可以确保该类或接口是 T 的生产者,而非消费者。

kotlin 复制代码
interface Source<out T> {
    fun nextT(): T
}

val stringSource: Source<String> = object : Source<String> {
    override fun nextT(): String = "Hello"
}

// 由于协变,这里赋值是合法的。
val anySource: Source<Any> = stringSource

在这个例子中,Source 接口的类型参数 T 被声明为 out,所以 Source<String> 可以安全地赋值给 Source<Any>,因为 Source 只生产 T 类型的对象,不会消费它们。

声明处型变:in(逆变)

当一个类型参数 T 被声明为 in 时,意味着该类型参数只能在类或接口的成员中作为参数类型出现,而不能作为返回类型。这样可以确保该类或接口是 T 的消费者,而非生产者。

kotlin 复制代码
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

val numberComparable: Comparable<Number> = object : Comparable<Number> {
    override fun compareTo(other: Number): Int = this.toDouble().compareTo(other.toDouble())
}

// 由于逆变,这里赋值是合法的。
val doubleComparable: Comparable<Double> = numberComparable

在这个例子中,Comparable 接口的类型参数 T 被声明为 in,所以 Comparable<Number> 可以安全地赋值给 Comparable<Double>,因为 Comparable 只消费 T 类型的对象,不会生产它们。

使用处型变:类型投影

将类型参数 T 声明为 out 并避免在使用处出现子类型问题是很容易的,但有些类实际上不能仅限于只返回 T 类型的值。Array 就是一个很好的例子:

kotlin 复制代码
class Array<T>(val size: Int) {
    operator fun get(index: Int): T { ... }
    operator fun set(index: Int, value: T) { ... }
}

这个类在 T 上既不能是协变的,也不能是逆变的。这就带来了一定的局限性。考虑以下函数:

kotlin 复制代码
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

这个函数的作用是将元素从一个数组复制到另一个数组。让我们在实际中尝试使用它:

kotlin 复制代码
val ints: Array<Int> = arrayOf(1, 2, 3)
val anys = Array<Any>(3) { "" }

// ints 类型是 Array<Int>,但期望的是 Array<Any>。
copy(ints, anys)

在这里,你又遇到了熟悉的问题:Array<T>T 上是不变的,所以 Array<Int>Array<Any> 都不是对方的子类型。为什么呢?这是因为 copy 函数可能会有意外的行为,例如,它可能会尝试向 from 数组中写入一个 String,而如果你实际传入的是一个 Int 数组,稍后就会抛出 ClassCastException 异常。

为了禁止 copy 函数向 from 数组写入数据,你可以这样做:

kotlin 复制代码
fun copy(from: Array<out Any>, to: Array<Any>) { ... }

这就是类型投影,意味着 from 不是一个普通的数组,而是一个受限(投影)的数组。你只能调用那些返回类型参数 T 的方法,在这种情况下,意味着你只能调用 get() 方法。这就是我们实现使用处型变的方式,它对应于 JavaArray<? extends Object>,只是稍微简单一些。

你也可以使用 in 来进行类型投影:

kotlin 复制代码
fun fill(dest: Array<in String>, value: String) { ... }

Array<in String> 对应于 JavaArray<? super String>。这意味着你可以将一个 String 数组、CharSequence 数组或 Object 数组传递给 fill() 函数。

kotlin 复制代码
fun copy(from: Array<out Any>, to: Array<in Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3) { "" }
    copy(ints, any)
}

使用处型变:* 投影

有时候,你对类型参数一无所知,但仍然希望以安全的方式使用它。这里的安全方式是定义泛型类型的这样一种投影,使得该泛型类型的每个具体实例化都是该投影的子类型。

Kotlin 为此提供了所谓的 * 投影语法:

  • 对于 Foo<out T : TUpper>,其中 T 是具有上界 TUpper 的协变类型参数,Foo<*> 等价于 Foo<out TUpper>。这意味着当 T 未知时,你可以安全地从 Foo<*> 中读取 TUpper 类型的值。

  • 对于 Foo<in T>,其中 T 是逆变类型参数,Foo<*> 等价于 Foo<in Nothing>。这意味着当 T 未知时,你无法以安全的方式向 Foo<*> 中写入数据。

  • 对于 Foo<T : TUpper>,其中 T 是具有上界 TUpper 的不变类型参数,Foo<*> 在读取值时等价于 Foo<out TUpper>,在写入值时等价于 Foo<in Nothing>

如果一个泛型类型有多个类型参数,每个参数都可以独立进行投影。例如,如果类型被声明为 interface Function<in T, out U>,你可以使用以下 * 投影:

  • Function<*, String> 表示 Function<in Nothing, String>

  • Function<Int, *> 表示 Function<Int, out Any?>

  • Function<*, *> 表示 Function<in Nothing, out Any?>

* 投影非常类似于 Java 的原始类型,但更安全。

kotlin 复制代码
// 定义一个协变的泛型类。
class Box<out T : Number>(val value: T)

fun main() {
    // 创建一个包含 Int 类型的 Box。
    val intBox: Box<Int> = Box(10)

    // 使用 * 投影。
    val starBox: Box<*> = intBox

    // 可以安全地读取 Number 类型的值。
    val number: Number = starBox.value

    println(number)
}

在这个例子中,Box<out T : Number> 是一个协变的泛型类。Box<*> 等价于 Box<out Number>,因此可以安全地从 starBox 中读取 Number 类型的值。

kotlin 复制代码
// 定义一个逆变的泛型类。
class Printer<in T> {
    fun print(value: T) {
        println(value)
    }
}

fun main() {
    // 创建一个 Printer<Int> 实例。
    val intPrinter: Printer<Int> = Printer()

    // 使用 * 投影。
    val starPrinter: Printer<*> = intPrinter

    // 无法安全地向 starPrinter 中写入值。
    // 这行代码会编译错误。
    // starPrinter.print(10)
}

在这个例子中,Printer<in T> 是一个逆变的泛型类。Printer<*> 等价于 Printer<in Nothing>,因此无法安全地向 starPrinter 中写入任何值。

kotlin 复制代码
// 定义一个不变的泛型类。
class Container<T : CharSequence>(var value: T)

fun main() {
    // 创建一个包含 String 类型的 Container。
    val stringContainer: Container<String> = Container("Hello")

    // 使用 * 投影。
    val starContainer: Container<*> = stringContainer

    // 可以安全地读取 CharSequence 类型的值。
    val charSequence: CharSequence = starContainer.value
    println(charSequence)

    // 无法安全地向 starContainer 中写入值。
    // 这行代码会编译错误。
    // starContainer.value = "World"
}

在这个例子中,Container<T : CharSequence> 是一个不变的泛型类。Container<*> 在读取值时等价于 Container<out CharSequence>,在写入值时等价于 Container<in Nothing>,因此可以安全地读取 CharSequence 类型的值,但无法安全地写入值。

kotlin 复制代码
// 定义一个带有两个类型参数的泛型接口。
interface Function<in T, out U> {
    fun apply(arg: T): U
}

fun main() {
    // 定义一个实现 Function<Int, String> 的匿名类。
    val intToStringFunction: Function<Int, String> = object : Function<Int, String> {
        override fun apply(arg: Int): String {
            return arg.toString()
        }
    }

    // 使用 * 投影。
    val function1: Function<*, String> = intToStringFunction

    // Function<*, String> 等价于 Function<in Nothing, String>,
    // 无法调用 apply 方法。
    // 这行代码会编译错误。
    // function1.apply(10)

    // 使用 * 投影。
    val function2: Function<Int, *> = intToStringFunction

    // Function<Int, *> 等价于 Function<Int, out Any?>,
    // 可以调用 apply 方法。
    val result: Any? = function2.apply(10)
    println(result)

    // 使用 * 投影。
    val function3: Function<*, *> = intToStringFunction

    // Function<*, *> 等价于 Function<in Nothing, out Any?>。
    // 无法调用 apply 方法。
    // 这行代码会编译错误。
    // function3.apply(10)
}

在这个例子中,Function<in T, out U> 是一个带有两个类型参数的泛型接口。通过不同的 * 投影方式,可以安全地处理不同的使用场景。

相关推荐
robotx2 小时前
安卓线程相关
android
消失的旧时光-19432 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon3 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon3 小时前
VSYNC 信号完整流程2
android
dalancon3 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013844 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android5 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才5 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶6 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙6 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github