首页 文章

Kotlin:泛型和差异

提问于
浏览
3

我想在 Throwable 上创建一个扩展函数,给定 KClass ,递归搜索与参数匹配的根本原因 . 以下是一种有效的尝试:

fun <T : Throwable> Throwable.getCauseIfAssignableFrom(e: KClass<T>): Throwable? = when {
    this::class.java.isAssignableFrom(e.java) -> this
    nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e)
    else -> null
}

这也有效:

fun Throwable.getCauseIfAssignableFrom(e: KClass<out Throwable>): Throwable? = when {
    this::class.java.isAssignableFrom(e.java) -> this
    nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e)
    else -> null
}

我这样调用函数: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class) .

但是,Kotlin docs关于仿制药说:

这称为声明站点方差:我们可以注释Source的类型参数T,以确保它仅从Source的成员返回(生成),并且从不消耗 . 为此,我们提供了out修饰符

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

在我的例子中,参数 e 不会被返回,而是被消耗 . 在我看来它应该声明为 e: KClass<in Throwable> 但不编译 . 但是,如果我认为 out 为"you can only read from, or return, it"和 in 为"you can only write, or assign a value, to it",那么它是有道理的 . 谁能解释一下?

3 回答

  • 2

    在您的情况下,您实际上并未使用类型参数的方差:您永远不会传递值或使用从调用 e: KClass<T> 返回的值 .

    方差描述了您可以作为参数传递的值以及在处理投影类型时(例如在函数实现中)从属性和函数返回的值可以期望的值 . 例如,如果 KClass<T> 将返回 T (如签名中所写), KClass<out SomeType> 可以返回 SomeType 或其任何子类型 . 相反,如果 KClass<T> 期望 T 的参数, KClass<in SomeType> 期望 SomeType 的一些超类型(但确切地说它是未知的) .

    实际上,这定义了传递给此类函数的实例的实际类型参数的限制 . 对于不变类型 KClass<Base> ,您无法传递 KClass<Super>KClass<Derived> (其中 Derived : Base : Super ) . 但是如果一个函数需要 KClass<out Base> ,那么你也可以传递 KClass<Derived> ,因为它满足上述要求:它从其方法返回 Derived ,该方法应返回 Base 或其子类型(但对于 KClass<Super> 则不是这样) . 而且,相反,期望 KClass<in Base> 的函数也可以接收 KClass<Super>

    因此,当您重写 getCauseIfAssignableFrom 以接受 e: KClass<in Throwable> 时,您声明在实现中您希望能够将 Throwable 传递给某个泛型函数或 e 的属性,并且您需要一个能够处理它的 KClass 实例 . Any::classThrowable::class 适合,但这不是你需要的 .

    由于你没有调用它的任何属性,你甚至可以使它的类型 KClass<*> (明确说明你不关心什么是类型并允许它成为任何东西),它会起作用 .

    但是您的用例要求您将类型限制为 Throwable 的子类型 . 这是 KClass<out Throwable> 的工作原理:它将类型参数限制为 Throwable 的子类型(同样,您声明,对于 KClass<T> 的函数和属性,返回 TT 的内容,如 Function<T> ,您希望使用返回值,就好像 TThrowable 的子类型;虽然你没有这样做) .

    适用于您的另一个选项是定义上限 <T : Throwable> . 这类似于 <out Throwable> ,但它还捕获 KClass<T> 的类型参数,并允许您在签名中的其他位置(在返回类型或其他参数的类型中)或实现内部使用它 .

  • 1

    其他答案已经解决了为什么你不需要在这个 use 网站上的差异 .

    仅供参考,如果您将回报转换为预期类型,API将更有用,

    @Suppress("UNCHECKED_CAST")
    fun <T : Any> Throwable.getCauseIfInstance(e: KClass<T>): T? = when {
        e.java.isAssignableFrom(javaClass) -> this as T
        else -> cause?.getCauseIfInstance(e)
    }
    

    但是使用一种具体的类型更像Kotlin .

    inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? =
        generateSequence(this) { it.cause }.filterIsInstance<T>().firstOrNull()
    

    这实际上与编写显式循环相同,但更短 .

    inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? {
        var current = this
        while (true) {
            when (current) {
                is T -> return current
                else -> current = current.cause ?: return null
            }
        }
    }
    

    与原版不同,此方法不需要 kotlin-reflect .

    (我也将行为从 isAssignableFrom 更改为 isinstanceof );我很难想象原作如何有用 . )

  • 1

    在您从文档中引用的示例中,您将显示带有 out 注释的通用 class . 这个注释为类的用户提供了一个保证,即除了从T派生的T或类之外,该类不会放出任何东西 .

    在您的代码示例中,您将在参数的类型上显示带有 out 注释的泛型函数 parameter . 这给参数的用户保证参数不会是 T (在你的情况下是a KClass<Throwable> )或派生自 T (a KClass<{derived from Throwable}> )的类 .

    现在改变 in 的想法 . 如果您要使用 e: KClass<in Throwable> ,则将参数约束为 Throwable 的超级 .

    在编译器错误的情况下,您的函数是否使用 e 的方法或属性并不重要 . 在您的情况下,参数的声明限制了函数的调用方式,而不是函数本身如何使用参数 . 因此,使用 in 而不是 out 将使用参数 NorRemoteRepositoryException::class 取消对函数的调用 .

    当然,约束也适用于您的函数,但这些约束永远不会被执行,因为 e 不是那样使用的 .

相关问题