Kotlin 语言中的泛型(Generics)形变主要指的是泛型在继承和实现接口时的行为,尤其是类型参数如何在这些情况下被处理。Kotlin 的泛型系统比 Java 更为严格和灵活,尤其是在类型协变(Covariance)和逆变(Contravariance)方面。
协变(Covariance)
协变是指子类型可以替换为其父类型。在 Kotlin 中,使用 out
关键字在泛型类型参数前标记为协变。这主要用在只输出(如返回)类型参数的场景中。
示例:
// 定义一个协变的泛型接口
interface Producer<out T> {
fun produce(): T
}
class StringProducer : Producer<String> {
override fun produce(): String = "Hello, Kotlin!"
}
fun <T> consumeProducer(producer: Producer<T>): T = producer.produce()
fun main() {
val stringProducer: Producer<String> = StringProducer()
// 协变允许这里将 StringProducer 传递给期望 Producer<Any> 的函数
val anyProducer: Producer<Any> = stringProducer
val result: Any = consumeProducer(anyProducer) // 结果是 "Hello, Kotlin!"
println(result)
}
逆变(Contravariance)
逆变是指父类型可以替换为其子类型。在 Kotlin 中,使用 in
关键字在泛型类型参数前标记为逆变。这主要用在只输入(如参数)类型参数的场景中。
示例:
// 定义一个逆变的泛型接口
interface Consumer<in T> {
fun consume(t: T)
}
class StringConsumer : Consumer<String> {
override fun consume(t: String) {
println(t)
}
}
fun <T> processConsumer(consumer: Consumer<T>) {
// 假设有逻辑处理
}
fun main() {
val stringConsumer: Consumer<String> = StringConsumer()
// 逆变允许这里将 StringConsumer 传递给期望 Consumer<Any> 的函数
val anyConsumer: Consumer<Any> = stringConsumer
processConsumer(anyConsumer) // 这里可以安全地传递任何类型的参数给 StringConsumer
// 注意:实际调用 consume 方法时仍需要保证类型安全
}
注意:
- 在 Kotlin 中,默认情况下泛型参数既不是协变也不是逆变,而是不变的(Invariant)。这意味着你不能直接将一个泛型类型的子类型实例赋值给其父类型的泛型变量,除非明确使用
out
或in
关键字来声明协变或逆变。 - 在 Java 中,泛型是不变的,但在 Kotlin 中通过使用
out
和in
关键字,我们可以更灵活地处理泛型类型参数,尤其是在接口和函数类型中。 - 协变和逆变的使用需要谨慎,因为它们在提高灵活性的同时也可能引入类型安全问题。
在 Kotlin 中,当泛型参数没有被明确标记为协变(out
)或逆变(in
)时,它们被视为不变(Invariant)。不变意味着泛型类型在子类和父类之间不能相互替换,即使它们之间有明显的继承关系。这种严格性有助于防止在编译时难以察觉的类型错误。
以下是一个泛型不变的代码示例,展示了如何在 Kotlin 中定义和使用一个不变的泛型类,并解释为什么它是不变的。
泛型不变的类定义
// 定义一个泛型类 Box,其中 T 是泛型参数,且没有使用 out 或 in 关键字
class Box<T>(var value: T) {
fun getValue(): T = value
fun setValue(newValue: T) {
value = newValue
}
}
// 子类尝试继承 Box 类并使用更具体的类型参数,但这并不会改变泛型的不变性
class StringBox(value: String) : Box<String>(value)
// 注意:这里 StringBox 并没有为 Box 引入
新的泛型不变性规则,
// 它只是简单地指定了 T 为 String 类型。
泛型不变的使用
fun main() {
val stringBox: Box<String> = StringBox("Hello, Kotlin!")
// 尝试将 stringBox 赋值给一个 Box<Any> 类型的变量将会失败,
// 因为 Box<T> 是不变的,所以 Box<String> 不是 Box<Any> 的子类型
// val anyBox: Box<Any> = stringBox // 这行会编译错误
// 但是,你可以这样做,因为它没有违反类型安全
val anyBox: Box<out Any> = stringBox // 使用 out 关键字使 Box 协变
// 注意:尽管这样可以编译,但你不能通过 anyBox 调用 setValue 方法,
// 因为协变只允许你读取值(通过 getValue),而不允许你写入值(setValue 需要一个具体的 T 类型参数)
// 读取值是安全的
val value: Any? = anyBox.getValue() // 这行是安全的,因为 getValue 不接受任何参数
// 写入值是不安全的,所以你不能这样做(除非 anyBox 是 Box<Any> 而不是 Box<out Any>)
// anyBox.setValue("This will not compile") // 这行会编译错误,因为 setValue 需要具体的 T 类型参数
}
总结
在 Kotlin 中,泛型默认是不变的,这意味着泛型类型参数在继承和实现接口时不能自动地替换为更广泛或更具体的类型。这有助于防止类型错误,但也可能需要额外的类型转换或使用协变和逆变来提高代码的灵活性和可用性。在上述示例中,Box<T>
类是不变的,但你可以通过显式地将泛型参数标记为协变(使用 out
关键字)来放宽某些限制,但这样做会限制你可以对泛型类型执行的操作(例如,你不能在协变泛型类型上调用需要具体类型参数的方法)。