Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the purpose of having bound class reference return a covariant type?

I'm playing with reflection and I came out with this problem. When using bound class reference via the ::class syntax, I get a covariant KClass type:

fun <T> foo(entry: T) {
    with(entry::class) {
       this // is instance of KClass<out T>
    }
}

As I could learn from the docs, this will return the exact type of the object, in case it is instance of a subtype of T, hence the variance modifier. However this prevents retrieving properties declared in the T class and getting their value (which is what I'm trying to do)

fun <T> foo(entry: T) {
    with(entry::class) {
       for (prop in memberProperties) {
           val v = prop.get(entry) //compile error: I can't consume T
       }
    }
}

I found that a solution is using javaClass.kotlin extension function on the object reference, to get instead the invariant type:

fun <T> foo(entry: T) {
    with(entry.javaClass.kotlin) {
       this // is instance of KClass<T>
    }
}

This way, I get both the exact type at runtime and the possibility to consume the type.

Interestingly, if I use a supertype instead of a generic, with the latter method I still get access to the correct type, without the need of variance:

class Derived: Base()

fun foo(entry: Base) {
    with(entry.javaClass.kotlin) {
       println(this == Derived::class)
    }
}

fun main(args: Array<String>) {
    val derived = Derived()
    foo(derived) // prints 'true'
}

If I got it correct, ::class is equal to calling the java getClass, which returns a variant type with a wildcard, while javaClass is a getClass with a cast to the specific type. Still, I don't get why would I ever need a covariant KClass, when it limits me to only produce the type, given that there are other ways to access the exact class at runtime and use it freely, and I wonder if the more immediate ::class should return an invariant type by design.

like image 204
devrocca Avatar asked Oct 17 '22 07:10

devrocca


1 Answers

The reason for covariance in bound ::class references is, the actual runtime type of an object the expression is evaluated to might differ from the declared or inferred type of the expression.

Example:

open class Base
class Derived : Base()

fun someBase(): Base = Derived()

val kClass = someBase()::class

The expression someBase() is typed as Base, but at runtime it's a Derived object that it gets evaluated to.

Typing someBase()::class as invariant KClass<Base> is simply incorrect, in fact, the actuall result of evaluating this expression is KClass<Derived>.

To solve this possible inconsistency (that would lead to broken type-safety), all bound class references are covariant: someBase()::class is KClass<out Base>, meaning that at runtime someBase() might be a subtype of Base, and therefore this might be a class token of a subtype of Base.

This is, of course, not the case with unbound class references: when you take Base::class, you know for sure that it's the class token of Base and not of some of its subtypes, so it's invariant KClass<Base>.

like image 174
hotkey Avatar answered Oct 21 '22 08:10

hotkey