Reputation: 24548
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?
Upvotes: 3
Views: 1353
Reputation: 204778
The other answers have addressed why you don't need variance at this use site.
FYI, the API would be more usable if you cast the return 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's more Kotlin-like to use a reified type.
inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? =
generateSequence(this) { it.cause }.filterIsInstance<T>().firstOrNull()
This is effectively 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
); I have a hard time imagining how the original could have been useful.)
Upvotes: 2
Reputation: 10605
In the example you cite from documentation, you are showing a generic class with the out
annotation. This annotation give users of the class a guarantee that the class will not put out anything but a T or class derived from T.
In you code example, you are showing a generic function parameter with an out
annotation on the parameter's type. This is giving users of the parameter the guarantee that the parameter will not be anything but a T
(in your case a KClass<Throwable>
) or a class derived from T
(a KClass<{derived from Throwable}>
).
Now reverse the thinking for in
. If you were to use e: KClass<in Throwable>
then you constrain the parameter to supers of Throwable
.
In the case of your compiler error, it isn't the point whether or not your function uses methods or properties of e
. In your case, the declaration of the parameter is constraining how the function can be called, not how the function itself uses the parameter. Therefore, using in
instead of out
disqualifies your call to the function with the parameter NorRemoteRepositoryException::class
.
Of course, the constraints also apply within your function, but those constraints are never exercised because e
is not used that way.
Upvotes: 1
Reputation: 147971
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.
Upvotes: 1