Kotlin - 泛型?还是泛型?

kotlin的泛型有in, out, where

概念

其中有3个概念,分别是协变,逆变,不变。

不变

我们这里有一段代码

class Box<T>(val value: T)

suspend fun main() {
    val stringBox: Box<String> = Box("Hello")
    val anyBox: Box<Any> = Box(42)

    // val anotherAnyBox: Box<Any> = stringBox  // BUILD FAILED
    // val anotherCsBox: Box<CharSequence> = stringBox // failed
}

在上述代码中,尝试将 Box<String> 赋值给 Box<Any> 会导致编译错误。因为在 Kotlin 中,泛型类型默认是不变的(invariant)。这意味着即使 StringAny 的子类型,Box<String> 也不能被认为是 Box<Any> 的子类型。这个设计是为了确保类型安全性,防止在类型转换时出现运行时错误。

协变

为了解决上面的这个奇怪问题,就得用协变了(out),协变指的是如果有一个类型T,它可以被替换为它或者它的子类型。

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

suspend fun main() {
    val stringBox: Box<String> = Box("Hello")
    val anyBox: Box<Any> = Box(42)

    val anotherAnyBox: Box<Any> = stringBox
}

这里Box<Any>,T为AnyAny可以替换为他的子类型String ,这就是协变。

逆变

接下来我们对Box进行一次小小的升级,然后喜提爆炸,

class Box<out T>(val value: T) {
    fun equal(other: T): Boolean { // build failed: Type parameter T is declared as 'out' but occurs in 'in' position in type T
        return value == other
    }
}

这段代码的问题在于 Box<out T> 中的 out 关键字。out 关键字表示这个类型参数 T 只能用作输出(返回值),不能用作输入(参数)。但是,在 equal 方法中,将 T 用作了输入参数,这违反了协变的规则。

解决方法

  • 消除out

  • 将other类型改为Any?

那么怎么样才能使用协变呢?假设有一个家庭聚会,

class FamilyInvitation<in T> {
    fun invite(member: T) {
        println("Inviting: $member")
    }
}

fun main() {
    val grandfatherInvitation: FamilyInvitation<Grandfather> = FamilyInvitation()
    val fatherInvitation: FamilyInvitation<Father> = grandfatherInvitation // 合法,因为 Grandfather 是 Father 的超类
    val sonInvitation: FamilyInvitation<Son> = fatherInvitation // 合法,因为 Father 是 Son 的超类

    sonInvitation.invite(Son()) // 可以向邀请名单中添加 Son 类型的成员
}

总结

  • 协变(Covariance):使用 out 关键字。如果我们有一个 Box<out T> 类型的盒子,那么 Box<Son> 可以被赋值给 Box<Father>,因为 SonFather 的子类。这意味着我们可以从盒子中获取 Father 类型的成员。

  • 逆变(Contravariance):使用 in 关键字。如果我们有一个 FamilyInvitation<in T> 类型的邀请名单,那么 FamilyInvitation<Grandfather> 可以被赋值给 FamilyInvitation<Father>,因为 GrandfatherFather 的超类。这意味着我们可以向邀请名单中添加 Father 类型的成员。

绝对不可空约束

import org.jetbrains.annotations.*;

public interface Game<T> {
    public T save(T x) {}
    @NotNull
    public T load(@NotNull T x) {}
}

这个是java的声明一个泛型非空,但是在kotlin这一过程大幅简化。

interface ArcadeGame<T1> : Game<T1> {
    override fun save(x: T1): T1
    // T1 is definitely non-nullable
    override fun load(x: T1 & Any): T1 & Any
}

当仅使用 Kotlin 时,不太可能需要明确声明绝对非可空类型,因为 Kotlin 的类型推断会自动处理这个问题。

具体化

当我们需要做一定的推断的时候,例如,我们需要一个Pair里面两个参数必须是xxx类型,可以通过inline函数使用reified去实现。

inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)


val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Compiles but breaks type safety!
// Expand the sample for more details



fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
    //println(stringToStringList?.second?.forEach() {it.length}) // This will throw ClassCastException as list items are not String
}