Kotlin:泛型和方差

Kotlin: generics and variance

我想在 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关于泛型的说法是:

This is called declaration-site variance: we can annotate the type parameter T of Source to make sure that it is only returned (produced) from members of Source, and never consumed. To do this we provide the out modifier

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",那么它就有意义了。有人可以解释一下吗?

在你的例子中,你实际上并没有使用类型参数的变化:你永远不会传递一个值或使用一个值 return 从调用你的 e: KClass<T>.

方差描述了哪些值可以作为参数传递,以及当您使用投影类型(例如在函数实现内部)时,您可以从属性和函数 returned 的值中得到什么。例如,如果 KClass<T> 会 return T(如签名中所写),KClass<out SomeType> 可以 return SomeType 或其任何子类型。相反,KClass<T> 期望 T 的参数,KClass<in SomeType> 期望 SomeType 的某些超类型(但是不知道具体是哪个)。

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

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

由于您不调用任何 e 的函数并且不访问它的任何属性,您甚至可以将其类型设为 KClass<*>(明确说明您不关心类型是什么并允许它是任何类型),它会起作用。

但是您的用例要求您将类型限制为 Throwable 的子类型。这就是 KClass<out Throwable> 起作用的地方:它将类型参数限制为 Throwable 的子类型(再次声明,对于 KClass<T> 的函数和属性, return T 或带有 T 的东西,比如 Function<T>,你想使用 return 值,就好像 TThrowable 的子类型一样;虽然你不不要这样做)。

另一个适合您的选项是定义上限 <T : Throwable>。这类似于 <out Throwable>,但它额外捕获了 KClass<T> 的类型参数,并允许您在签名中的其他地方使用它(在 return 类型或其他参数的类型中) 或在实现内部。

在您从文档中引用的示例中,您显示了带有 out 注释的通用 class。此注释向 class 的用户保证 class 除了 T 或从 T.class 派生的 class 之外不会输出任何内容。

在您的代码示例中,您显示了一个通用函数 parameter,在参数类型上带有 out 注释。这为参数的用户提供了保证,该参数将不会是 T(在您的情况下是 KClass<Throwable>)或从 T 派生的 class(a KClass<{derived from Throwable}>).

现在反过来考虑in。如果您要使用 e: KClass<in Throwable> 那么您将参数限制为 Throwable.

的超参数

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

的函数不合格

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

其他答案已经解决了为什么您不需要在此 使用 站点上的差异。

仅供参考,如果将 return 转换为预期类型,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 更改为 is (instanceof);我很难想象原来的行为会有多大用处。)