Kotlin: generics and variance
I want to create an extension function on Throwable
that, when given, KClass
recursively searches for the root cause that matches the argument. Below is one attempt that works:
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
}
This works too:
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
}
I call the function as follows: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class)
.
However, the Kotlin docs on generics says:
This is called the variance of the ad-site: we can annotate the type parameter T of the source to make sure that it is only returned (produced) from the members of the Source and never consumed. For this we provide an 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
// ...
}
In my case, the parameter is e
not returned, but consumed. It looks to me like it should be declared as e: KClass<in Throwable>
, but it doesn't compile. However, if I am thinking about out
how “you can read or return it” and in
how “you can write or assign a value to it”, then it makes sense. Can someone please explain?
source to share
In your case, you are not actually using the variance of the type parameter: you never pass a value or use the value returned when called on yours e: KClass<T>
.
, , , (, ). , a KClass<T>
a T
( ), a KClass<out SomeType>
SomeType
, . , a KClass<T>
T
, a KClass<in SomeType>
SomeType
( ).
This, in fact, defines the constraints on the actual type arguments of the instances that you pass to such a function. With an invariant type, KClass<Base>
you cannot pass KClass<Super>
or KClass<Derived>
(where Derived : Base : Super
). But if the function expects KClass<out Base>
, you can also pass KClass<Derived>
because it satisfies the aforementioned requirement: it returns Derived
from its methods which it should return Base
or its subtype (but this is not true for KClass<Super>
). Conversely, a function that expects KClass<in Base>
can also receiveKClass<Super>
So when you rewrite getCauseIfAssignableFrom
to accept e: KClass<in Throwable>
, you are specifying that in the implementation you want to pass to Throwable
some kind of generic function or property e
, and you need an a KClass
that is capable of handling that. Any::class
or Throwable::class
fit, but that's not what you want.
Since you don't call any of the functions e
or access any of its properties, you can even make it a type KClass<*>
(by clearly stating that you don't care what type it is and allow it to be anything) and it will work ...
But your use case requires you to restrict the type to a subtype Throwable
. It KClass<out Throwable>
works here : it restricts the type argument to a subtype Throwable
(again you are arguing that for functions and properties KClass<T>
that return T
or something T
like Function<T>
, you want to use the return value as if it were T
a subtype Throwable
, although you don't).
Another option that works for you is defining an upper bound <T : Throwable>
. This is similar to <out Throwable>
, but additionally captures the type argument KClass<T>
and allows it to be used elsewhere in the signature (in the return type or types of other parameters) or within the implementation.
source to share
Other answers have figured out why you don't need to deviate on this site .
FYI, the API will be more convenient if you revert back to the expected type,
@Suppress("UNCHECKED_CAST")
fun <T : Any> Throwable.getCauseIfInstance(e: KClass<T>): T? = when {
e.java.isAssignableFrom(javaClass) -> this as T
else -> cause?.getCauseIfInstance(e)
}
but it looks more like Kotlin to use the reified type.
inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? =
generateSequence(this) { it.cause }.filterIsInstance<T>().firstOrNull()
This is actually the same as writing an explicit loop, but shorter.
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
}
}
}
And unlike the original, this method does not require kotlin-reflect
.
(I've also changed the behavior from isAssignableFrom
to is
( instanceof
), it's hard for me to see how the original could be helpful.)
source to share
In the example you cite from the documentation, you are showing a generic annotated classout
. This annotation gives users of the class a guarantee that the class will not infer anything other than T or a class derived from T.
In the example code, you are showing a generic function annotated out
by parameter type. This gives the users of the parameter a guarantee that the parameter will have no value other than T
(in your case, a KClass<Throwable>
) or the class derived from T
(a KClass<{derived from Throwable}>
).
Now change your mindset to in
. If you used e: KClass<in Throwable>
then you are limiting the supers parameter Throwable
.
In the event of your compiler error, whether your function is using methods or properties e
. In your case, the parameter declaration restricts the way the function is called, not how the function itself uses that parameter. Therefore, using in
instead will out
disqualify your function call with the parameter NorRemoteRepositoryException::class
.
Of course, the constraints also apply in your function, but those constraints are never enforced because they are e
not used that way.
source to share