Alexey
Alexey

Reputation: 3192

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?

Upvotes: 8

Views: 12761

Answers (3)

Roland
Roland

Reputation: 23242

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.

Upvotes: 12

xinaiz
xinaiz

Reputation: 7788

If you want your map to be immutable after initialization, you could do this:

import kotlin.reflect.KClass

class InstanceKeyMapper(initBlock: InstanceKeyMapper.() -> Unit) {

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

    init {
        initBlock(this)
    }

    infix fun <T : Any> KClass<T>.instance(value: T) {
        map[this] = value
    }

    fun toMap() = map as Map<KClass<*>, Any> // downcast to disable mutability

}

fun instanceMapOf(initBlock: InstanceKeyMapper.() -> Unit) = InstanceKeyMapper(initBlock).toMap()

And use it the following way:

fun main(args: Array<String>) {

    val map = instanceMapOf {
        Int::class instance 42 // ok
        String::class instance "abc" // ok
        Float::class instance 3.14f // ok
        Boolean::class instance true // ok
        Long::class instance "not legit" // bad type, compilation error
    }

    println(map[Int::class]) // 2
    println(map[String::class]) // "abc"
    map[Long::class] = 123L // compilation error, read-only
}

Upvotes: 1

Laurence
Laurence

Reputation: 1666

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.

Upvotes: 2

Related Questions