Reputation: 18811
In Kotlin I can use filterIsInstance
to obtain a type-specific (and type-safe) sub-collection:
val misc: List<Any> = listOf(42, 3.14, true, "foo", "bar")
val strings: List<String> = misc.filterIsInstance<String>()
println(strings) // => [foo, bar]
But I have a large collection of objects and I would like to pre-sort it into a Map, by concrete type. Is it even possible to define such a map in Kotlin's type system?
val miscByType: Map<KType, Collection<???>>
or:
val miscByClass: Map<KClass, Collection<???>>
Should I use a custom implementation with unsafe (but logically sound) casts?
The following is such an implementation. It works, but I'm wondering if there is a less hacky way of doing it:
import kotlin.reflect.KClass
class InstanceMap {
// INVARIANT: map from KClass to a set of objects of *that concrete class*
private val map: MutableMap<KClass<*>, MutableSet<Any>> = mutableMapOf()
// this is the only public mutator, it guarantees the invariant
fun add(item: Any): Boolean =
map.getOrPut(item::class) { mutableSetOf() }.add(item)
// public non-inline accessor, only needed by the inline accessor
fun get(cls: KClass<*>): Set<*>? = map[cls]
// inline accessor that performs an unsafe, but sound, cast
@Suppress("UNCHECKED_CAST")
inline fun <reified T> get(): Set<T> = get(T::class) as Set<T>? ?: setOf()
}
fun instanceMapOf(vararg items: Any): InstanceMap = InstanceMap().apply {
items.forEach { add(it) }
}
val misc = instanceMapOf(42, 3.14, true, "foo", "bar")
val strings = misc.get<String>()
println(strings) // => [foo, bar]
Upvotes: 2
Views: 1307
Reputation: 4992
Your code looks OK. The only problem is with the unchecked cast warning. At the JVM bytecode level, the cast does nothing, because of the way, how generics are implemented in Java and Kotlin. It is also known as type erasure.
https://en.wikipedia.org/wiki/Type_erasure
Type erasure adds yet another problem to your code - it does not tell generic type arguments. So that, for example, List<Int>
has the same class as List<String>
or List<Map<String, Object>>
Do you expect your code to find superclasses or interfaces in the map? E.g. if I have
interface A
interface B
class C : A, B
val m = InstanceMap()
m.add(C())
m.get(C::class)
m.get(A::class)
m.get(B::class)
Do you expect these 3 calls to return the same value?
The JVM standard workaround for it is to explicitly pass Class<T>
parameter and use the Class#cast
method to cast instead. That change will make the code safer.
There is a remedy to type erasure in Kotlin. You may add a reified inline function so that Kotlin compiler will use the exact type in the inlined generic function body
https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters
inline fun <reified T> InstanceMap.get() = get(T::class)
Upvotes: 2