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> 是一个带有两个类型参数的泛型接口。通过不同的 * 投影方式,可以安全地处理不同的使用场景。

相关推荐
双鱼大猫1 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫1 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫1 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标2 小时前
android 快速定位当前页面
android
雾里看山5 小时前
【MySQL】内置函数
android·数据库·mysql
风浅月明5 小时前
[Android]页面间传递model列表
android
法迪5 小时前
Android自带的省电模式主要做什么呢?
android·功耗
风浅月明5 小时前
[Android]AppCompatEditText限制最多只能输入两位小数
android
没有晚不了安5 小时前
1.11作业
android
zhangphil5 小时前
Android Coil3缩略图、默认占位图placeholder、error加载错误显示,Kotlin(1)
android·kotlin