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 VB
s. 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>()