ZetCode

Kotlin out 关键字

最后修改于 2025 年 4 月 19 日

Kotlin 的型变系统为处理泛型提供了强大的工具。 out 关键字在类型参数中启用协变。 本教程将通过实际例子深入探讨 out 关键字。

基本定义

Kotlin 中的 out 关键字将类型参数标记为协变。 这意味着泛型类是类型参数的生产者。 协变类型可以在比最初声明的更具体的上下文中使用。

简单的协变类

一个基本的协变类在其类型参数上使用 out 修饰符。 这允许将具有更具体类型参数的实例分配给具有不太具体类型的引用。

SimpleCovariance.kt
package com.zetcode

class Box<out T>(val value: T)

fun main() {
    val stringBox = Box("Hello")
    val anyBox: Box<Any> = stringBox
    
    println(anyBox.value) // Output: Hello
}

在这里,我们声明一个协变的 Box 类,使用 out T。 我们可以将一个 Box<String> 分配给一个 Box<Any> 引用,因为 String 是 Any 的子类型。 out 关键字使这种分配成为可能。

协变 List 接口

Kotlin 的 List 接口在设计上是协变的。 它只产生元素(通过 get 操作),从不消耗它们(没有 add 操作)。 这使得它对于协变是安全的。

ListCovariance.kt
package com.zetcode

fun printList(list: List<Any>) {
    list.forEach { println(it) }
}

fun main() {
    val strings = listOf("A", "B", "C")
    printList(strings) // Works because List is covariant
}

printList 函数接受 List<Any>,但我们传递 List<String>。 这是可行的,因为 Kotlin 中的 List 声明为 interface List<out E>out 修饰符启用了这种安全的向上转型。

协变自定义接口

您可以创建自己的协变接口。 out 修饰符确保类型参数仅用于输出位置(返回类型)。

CustomInterface.kt
package com.zetcode

interface Producer<out T> {
    fun produce(): T
}

class StringProducer : Producer<String> {
    override fun produce() = "Hello"
}

fun main() {
    val producer: Producer<Any> = StringProducer()
    println(producer.produce()) // Output: Hello
}

Producer 接口在 T 上是协变的。 我们可以将一个 StringProducer 分配给一个 Producer<Any> 引用。 out 修饰符确保 T 仅用作返回类型,从而使赋值类型安全。

具有多个类型参数的协变类

一个类可以具有多个具有不同型变注释的类型参数。 只有一些参数可能是协变的,而其他参数保持不变。

MultipleParams.kt
package com.zetcode

class Response<out T, E>(val data: T, val error: E?)

fun main() {
    val stringResponse = Response("Success", null)
    val anyResponse: Response<Any, Nothing> = stringResponse
    
    println(anyResponse.data) // Output: Success
}

Response 类有一个协变参数 (T) 和一个不变参数 (E)。 我们可以将 Response<String, Nothing> 分配给 Response<Any, Nothing>,因为 T 上有 out 修饰符。 E 参数保持不变。

协变函数参数

函数参数也可以使用协变类型。 这在将参数传递给函数时提供了更大的灵活性。

FunctionParams.kt
package com.zetcode

fun processItems(items: List<out Number>) {
    items.forEach { println(it.toDouble()) }
}

fun main() {
    val ints = listOf(1, 2, 3)
    val doubles = listOf(1.1, 2.2, 3.3)
    
    processItems(ints)
    processItems(doubles)
}

processItems 函数接受 List<out Number>,这意味着它可以接受任何 Number 子类型的列表。 我们传递了 List<Int> 和 List<Double>。 out 投影使这成为可能。

协变数组

Kotlin 中的数组默认是不变的,但我们可以在需要时使用带有 out 的数组投影来实现协变。

ArrayCovariance.kt
package com.zetcode

fun printArray(array: Array<out Any>) {
    array.forEach { println(it) }
}

fun main() {
    val strings = arrayOf("A", "B", "C")
    printArray(strings) // Works due to out projection
}

虽然 Array<T> 是不变的,但我们可以在 printArray 函数中使用 out 投影来将其视为协变的。 这允许在需要 Array<out Any> 的地方传递 Array<String>。

协变返回类型

out 修饰符可用于在类层次结构中启用协变返回类型。 这允许在子类中使用更具体的返回类型。

ReturnTypes.kt
package com.zetcode

open class Animal
class Dog : Animal()

interface AnimalShelter<out T : Animal> {
    fun adopt(): T
}

class DogShelter : AnimalShelter<Dog> {
    override fun adopt() = Dog()
}

fun main() {
    val shelter: AnimalShelter<Animal> = DogShelter()
    val animal = shelter.adopt()
    println(animal::class.simpleName) // Output: Dog
}

AnimalShelter 接口在 T 上是协变的。 这允许 DogShelter 用作 AnimalShelter<Animal>。 out 修饰符通过将 T 限制为仅输出位置来确保类型安全。

使用 out 的最佳实践

来源

Kotlin 泛型文档

本教程深入探讨了 Kotlin 的 out 关键字,展示了它如何启用泛型中的协变。 我们探讨了各种场景,包括接口、类、函数参数和数组。 正确使用型变注释可以使您的 API 更加灵活,同时保持类型安全。

作者

我的名字是 Jan Bodnar,我是一位热情的程序员,拥有多年的编程经验。 自 2007 年以来,我一直在撰写编程文章。 到目前为止,我写了 1400 多篇文章和 8 本电子书。 我有超过八年的编程教学经验。

列出 所有 Kotlin 教程