Tobia
Tobia

Reputation: 18811

Type- or Class-keyed map

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

Answers (1)

Eugene Petrenko
Eugene Petrenko

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

Related Questions