I want to create an extension function on Throwable
that, given a KClass
, recursively searches for a root cause that matches the argument. The following 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 like so: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class)
.
However, Kotlin docs about generics says:
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
// ...
}
In my case, parameter e
isn't returned, but consumed. It seems to me that it should be declared as e: KClass<in Throwable>
but that doesn't compile. However, if I think of out
as "you can only read from, or return, it" and in
as "you can only write, or assign a value, to it", then it makes sense. Can someone explain?
In your case, you don't actually use the variance of the type parameter: you never pass a value or use a value returned from a call to your e: KClass<T>
.
The variance describes which values you can pass as an argument and what you can expect from the values returned from properties and functions when you work with the projected type (e.g. inside a function implementation). For example, where a KClass<T>
would return a T
(as written in the signature), a KClass<out SomeType>
can return either SomeType
or any of its subtypes. On contrary, where a KClass<T>
would expect an argument of T
, a KClass<in SomeType>
expects some of the supertypes of SomeType
(but it is unknown which exactly).
This, in fact, defines the limitations on the actual type arguments of the instances that you pass to such a function. With invariant type KClass<Base>
, you cannot pass a KClass<Super>
or KClass<Derived>
(where Derived : Base : Super
). But if a function expects KClass<out Base>
, then you can also pass a KClass<Derived>
, because it satisfies the aforementioned requirement: it returns Derived
from its methods which should return Base
or its subtype (but that's not true for KClass<Super>
). And, on contrary, a function that expects KClass<in Base>
can also receive KClass<Super>
So, when you rewrite getCauseIfAssignableFrom
to accept a e: KClass<in Throwable>
, you state that in the implementation you want to be able to pass a Throwable
to some generic function or a property of e
, and you need a KClass
instance that is capable of handling that. The Any::class
or Throwable::class
would fit, but that's not what you need.
Since you don't call any of e
's functions and don't access any of its properties, you could even make its type KClass<*>
(clearly stating that you don't care what is the type and allow it to be anything), and it would work.
But your use case requires you to restrict the type to be a subtype of Throwable
. This is where KClass<out Throwable>
works: it restricts the type argument to be a subtype of Throwable
(again, you state that, for the functions and properties of KClass<T>
that return T
or something with T
like Function<T>
, you want to use the return value as if T
is a subtype of Throwable
; though you don't do it).
The other option that works for you is defining an upper bound <T : Throwable>
. This is similar to <out Throwable>
, but it additionally captures the type argument of KClass<T>
and allows you to use it somewhere else in the signature (in the return type or the types of the other parameters) or inside the implementation.