【问题标题】:Kotlin: generics and varianceKotlin:泛型和方差
【发布时间】:2017-08-08 23:57:34
【问题描述】:

我想在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 的成员,从未消耗过。为此,我们提供 输出修饰符

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&lt;in Throwable&gt; 但这不会编译。但是,如果我将out 视为“您只能读取或返回它”,而将in 视为“您只能对其进行写入或赋值”,那么这是有道理的。谁能解释一下?

【问题讨论】:

  • 嘿,刚刚注意到您的递归函数是序列的主要候选者:generateSequence(this) { it.cause }.first { it::class.java.isAssignableFrom(e.java) } 应该是行为兼容的,IMO 更快地消耗精神并且还摆脱了递归。但不要编辑问题,因为它可能会使当前答案无效。
  • 您使用 Throwable 派生类调用您的方法,所以当您将参数定义为 KClass 时,您应该预期会出现编译错误,对吧?
  • @mEQ5aNLrK3lqs3kfSa5HbvsTWe0nIu 一些用户名!关于顺序,我看到的问题是先收集所有原因,然后返回匹配的原因(如果有)。但是如果我们找到匹配项,就不需要再迭代了;您声称序列比递归更快的说法在理论上似乎并不成立。
  • @AbhijitSarkar Sequence 是懒惰的,它不会继续迭代。
  • 通过编写测试验证该序列确实是惰性求值的。谢谢。

标签: java generics kotlin covariance


【解决方案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 更改为 is (instanceof);我很难想象原来的行为会如何有用。)

【讨论】:

  • 对于具有具体类型参数且没有高阶函数参数的内联函数,最好将逻辑提取到一个非内联函数中,采用额外的 Java Class,然后简单地制作内联函数调用该函数。如果函数逻辑增长并在许多地方使用,这将防止字节码爆炸。
  • 您的实现中的e 形式参数在哪里?
  • @AbhijitSarkar 没有必要。当您编写 .getCauseIfInstance&lt;T&gt;() 时,编译器会填写其余部分。
【解决方案2】:

在您的情况下,您实际上并没有使用类型参数的方差:您从不传递值或使用从对您的e: KClass&lt;T&gt; 的调用返回的值。

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

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

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

由于您不调用e 的任何函数并且不访问其任何属性,您甚至可以将其类型设为KClass&lt;*&gt;(明确说明您不关心类型和允许它是任何东西),它会工作。

但是您的用例要求您将类型限制为Throwable 的子类型。这就是KClass&lt;out Throwable&gt; 起作用的地方:它将类型参数限制为Throwable 的子类型(再次声明,对于返回KClass&lt;T&gt; 的函数和属性TT 之类的东西@ 987654356@,你想使用返回值,好像TThrowable 的子类型;虽然你不这样做)。

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

【讨论】:

  • 我把你的解释读了 3 遍,你说的是真的,但我不确定它是否能回答我的问题。正如你所说,我不使用e 中的任何属性或方法,除了获取java 类。似乎in 也应该可以工作,但事实并非如此。你能帮我理解为什么不吗?
  • @AbhijitSarkar,这是因为当您定义 KClass&lt;in Throwable&gt; 类型的参数时,您会限制您可以传递的参数,以便它们都应该能够接收到 Throwable,而 KClass&lt;T&gt; 期望T(您希望能够传递Throwable in)。例如,Any::class 满足这个限制:因为它可以接收Any 作为T,它也可以接收Throwable。但NoRemoteRepositoryException::class 没有:它只期望NoRemoteRepositoryException 及其子类型为T,因此它不知道如何处理任意Throwable
  • 您回答的冗长实际上是我理解的障碍 :) 我更喜欢简短而中肯的答案。但我理解每个人都有自己的解释和理解方式。
【解决方案3】:

在您从文档中引用的示例中,您展示了一个带有out 注释的通用。此注解为该类的用户提供了保证,即该类除了 T 或从 T 派生的类之外不会输出任何内容。

在您的代码示例中,您展示了一个泛型函数 parameter,在参数类型上带有 out 注释。这为参数的用户提供了保证,该参数除了 T(在您的情况下为 KClass&lt;Throwable&gt;)或派生自 T 的类(KClass&lt;{derived from Throwable}&gt;)之外不会是任何东西。

现在扭转in 的想法。如果您要使用e: KClass&lt;in Throwable&gt;,则将参数限制为Throwable 的上级。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-18
    • 2021-01-05
    • 2020-11-20
    • 2011-02-09
    • 1970-01-01
    • 2012-09-29
    相关资源
    最近更新 更多