Home > front end >  Casts of generic classes in mutableMap
Casts of generic classes in mutableMap

Time:09-17

In my application I want to do something like this:

interface Binder<in VB: ViewBinding, in T: Any> {
    fun bind(binding: VB, item: T)
}

class TypeInfoMap {
    val map = mutableMapOf<Class<out Any>, Binder<ViewBinding, Any>>()

    inline fun <reified VB: ViewBinding, reified T: Any> put(binder: Binder<VB, T>) {
        map[T::class.java] = binder as Binder<ViewBinding, Any> // warning unchecked cast
    }

    inline fun <reified VB: ViewBinding, reified T: Any> get(cls: Class<T>): Binder<VB, T> {
        return map[cls] as? Binder<VB, T> ?: throw IllegalStateException() // no warning
    }
}

I get the warning unchecked cast in the put function. Why is that? I declared upper bounds for the generic types, shouldn't the cast be fine here? Also the cast in the get function does not produce any warning, even when I don't inline the function. I would have thought I would get a warning here and I'm actually surprised that I don't get one.

Is there a way in Kotlin to write all of this without warnings? I thought that's what reified is for.

CodePudding user response:

First of all, it is incorrect to cast from Binder<VB, T> to Binder<ViewBinding, Any>. If binder is defined as a Binder<VB, T>, you can call binder.bind() with a VB instance, but not with ViewBinding instances that are not VBs. So a Binder<VB, T> is not a Binder<ViewBinding, Any>.

Second, the unchecked cast warning is not about whether the cast is valid or not. It's about the fact that you won't get a ClassCastException at runtime if the type is not correct. This is why it's dangerous.

You probably don't get an unchecked cast warning in the get() method because the cast is always valid anyway: a Binder<ViewBinding, Any> is always a Binder<VB, T> given the variance of Binder and the declared parent types of VB and T.

Is there a way in Kotlin to write all of this without warnings? I thought that's what reified is for.

reified allows to access the generic types at runtime, but only those of the reified function. So for instance they allow you to get the KClass of some instance without explicitly passing it. However, the internal map you're using will still give no information at runtime about what you put in it in the past.

The best you can do is ignore the unchecked cast warning because you won't be able to know the generic types contained in the map at runtime. However, you have a more type-safe approach if you make the map private because you can control what you put in it:

class TypeInfoMap {
    private val map = mutableMapOf<Key<*, *>, Binder<*, *>>()

    class Key<VB : ViewBinding, T : Any>(
        val bindingClass: KClass<VB>,
        val valueClass: KClass<T>,
    )

    fun <VB : ViewBinding, T : Any> put(key: Key<VB, T>, binder: Binder<VB, T>) {
        map[key] = binder
    }

    @Suppress("UNCHECKED_CAST") // types are guaranteed by put()
    fun <VB : ViewBinding, T : Any> get(key: Key<VB, T>): Binder<VB, T> {
        val binder = map[key] ?: error("No binding of type ${key.bindingClass} found for class ${key.valueClass}")
        return binder as Binder<VB, T>
    }

    inline fun <reified VB: ViewBinding, reified T: Any> put(binder: Binder<VB, T>) {
        put(Key(VB::class, T::class), binder)
    }

    inline fun <reified VB: ViewBinding, reified T: Any> get(): Binder<VB, T> = get(Key(VB::class, T::class))
}

You can then use it safely with nice reified types:

val infoMap = TypeInfoMap()

val someBinder: Binder<MyViewBinding, MyType> = createSomeBinderSomewhere()
infoMap.put(someBinder)

// guaranteed type here (or runtime error if no binding of those types is found)
val binder = infoMap.get<MyViewBinding, MyType>()
  • Related