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?

+3


source to share


3 answers


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.

+1


source


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.)

+2


source


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.

+1


source







All Articles