Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create map with generics in Kotlin?

I need to create a Map where keys are classes, and values are objects of appropriate classes.

Like:

mapOf<KClass<T>, T>(
    Int::class to 10,
    String::class to "Ten"
)

I want to use generics to avoid 'invalid' entries, like Int::class to "Ten"

How can I implement it?

like image 228
Alexey Avatar asked Dec 13 '22 12:12

Alexey


2 Answers

I am not so sure whether I get what you really want to accomplish. Don't forget that generics are erased at runtime so at the end you will just have a Map<KClass<*>, Any> (more correctly: Map<Any, Any>). Nonetheless, the probably easiest way is to just stick to what you already know. You already showed a convenience method (to) to create a Pair which is then passed to the mapOf, so why not just use a new function that adheres to your requirements, e.g.

inline fun <reified T : Any> typedPair(value : T) = Pair(T::class, value)

So you can use:

mapOf(
  typedPair(10), // adds Int::class as key with 10 as value
  typedPair<Short>(1) // adds Short::class as key with 1 as value
  typedPair<Number>(2) // adds Number::class as key with 2 as value
)

Of course this way you are still able to add any other constellation into that map. If you want to overcome that, you have still some different options available:

What about creating an additional typedMapOf function, e.g.:

fun typedMapOf(vararg values : Any) = values.associateBy { it::class }

Using it could look as follows:

typedMapOf(10, "string", 1.toShort())

However you probably will have a hard time adding Number::class then ;-)

You can also mix the two variants above to something like:

data class MyTypedPair<T : Any>(val type : KClass<T>, val value : T)
inline fun <reified T : Any> typedPair(value : T) = MyTypedPair(T::class, value)
fun typedMapOf(vararg values : MyTypedPair<*>) = values.associateBy({it.type}) { it.value }

Which basically now forces you to deliver the specialized type to create that typed map.

I still have some other variants... You can also have something like a wrapper that just supports a minimal set of functions:

class MyValues {
    private val backedMap = mutableMapOf<KClass<*>, Any>()
    fun <T : Any> put(value : T) = backedMap.put(value::class, value)
    operator fun <T : Any> get(key : KClass<T>) = backedMap[key]
}

Usage then differs a bit from the Map but is still very easy:

MyValues().apply {
  put(10)
  put<Short>(1)
}

And if the type isn't derivable from the value then you can still use the above to construct a solution that probably fits your needs.

like image 141
Roland Avatar answered Dec 17 '22 23:12

Roland


Your example using generics does not actually describe your goal for that map. Generics on the Map interface are not capable of describing the sort of functionality you want. The types of the keys and the values would need to encapsulate every key and value you put in that map, so, this is possible:

val myInstanceMap = mapOf<KClass<*>, Any>(
        Int::class to 10,
        String::class to "10"
)

To get the type safety around particular keys and values in that map, you would have to do some of your own work to wrap such a general map. Here is an example:

class ClassToInstanceMap {

    private val backingMap = mutableMapOf<KClass<*>, Any?>()

    operator fun <T: Any> set(key: KClass<T>, value: T) {
        backingMap[key] = value
    }

    @Suppress("UNCHECKED_CAST")
    operator fun <T: Any> get(key: KClass<T>): T {
        return backingMap[key] as T
    }

    fun containsKey(key: KClass<*>): Boolean {
        return backingMap.containsKey(key)
    }

}

fun main() {
    val classToInstanceMap = ClassToInstanceMap()

    classToInstanceMap[Int::class] = 1
    val intInstance = classToInstanceMap[Int::class]
    println(intInstance)

    classToInstanceMap[Int::class] = 2
    val intInstance2 = classToInstanceMap[Int::class]
    println(intInstance2)

    classToInstanceMap[String::class] ="1"
    val stringInstance = classToInstanceMap[String::class]
    println(stringInstance)

    classToInstanceMap[String::class] ="2"
    val stringInstance2 = classToInstanceMap[String::class]
    println(stringInstance2)
}

I am sure you can figure out how to implement the other general methods of a map from that.

like image 39
Laurence Avatar answered Dec 18 '22 00:12

Laurence