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::class
或 Throwable::class
会适合,但这不是您需要的。
由于您不调用任何 e
的函数并且不访问它的任何属性,您甚至可以将其类型设为 KClass<*>
(明确说明您不关心类型是什么并允许它是任何类型),它会起作用。
但是您的用例要求您将类型限制为 Throwable
的子类型。这就是 KClass<out Throwable>
起作用的地方:它将类型参数限制为 Throwable
的子类型(再次声明,对于 KClass<T>
的函数和属性, return T
或带有 T
的东西,比如 Function<T>
,你想使用 return 值,就好像 T
是 Throwable
的子类型一样;虽然你不不要这样做)。
另一个适合您的选项是定义上限 <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
);我很难想象原来的行为会有多大用处。)
我想在 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::class
或 Throwable::class
会适合,但这不是您需要的。
由于您不调用任何 e
的函数并且不访问它的任何属性,您甚至可以将其类型设为 KClass<*>
(明确说明您不关心类型是什么并允许它是任何类型),它会起作用。
但是您的用例要求您将类型限制为 Throwable
的子类型。这就是 KClass<out Throwable>
起作用的地方:它将类型参数限制为 Throwable
的子类型(再次声明,对于 KClass<T>
的函数和属性, return T
或带有 T
的东西,比如 Function<T>
,你想使用 return 值,就好像 T
是 Throwable
的子类型一样;虽然你不不要这样做)。
另一个适合您的选项是定义上限 <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
);我很难想象原来的行为会有多大用处。)