如何在Kotlin中使用泛型创建地图?

时间:2019-04-24 21:13:23

标签: generics kotlin

我需要创建一个Map,其中键是类,值是适当类的对象。

赞:

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

我想使用泛型来避免“无效”条目,例如Int::class to "Ten"

我该如何实施?

3 个答案:

答案 0 :(得分:2)

您使用泛型的示例实际上并未描述该地图的目标。 Map界面上的泛型无法描述所需的功能。键的类型和值需要封装您放入该映射中的每个键和值,因此,这是可能的:

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

要获得该映射中特定键和值的类型安全性,您必须做一些自己的工作来包装这种通用映射。这是一个示例:

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)
}

我确信您可以从中弄清楚如何实现地图的其他常规方法。

答案 1 :(得分:2)

我不确定我是否能得到您真正想要实现的目标。不要忘记,泛型会在运行时删除,因此最后您将只有一个Map<KClass<*>, Any>(更正确的是:Map<Any, Any>)。尽管如此,最简单的方法可能就是坚持您已经知道的内容。您已经展示了一种方便的方法(to)创建一个Pair,然后将其传递给mapOf,所以为什么不使用符合您要求的新功能,例如

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

因此您可以使用:

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
)

当然,通过这种方式,您仍然可以在该地图中添加其他任何星座。如果您想克服这个问题,您仍然可以使用其他选项:

如何创建附加的typedMapOf函数,例如:

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

使用它可能如下所示:

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

但是,您可能很难添加Number::class然后再添加;-)

您还可以将上面的两个变体混合在一起,例如:

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 }

现在基本上迫使您交付专用类型来创建该类型化地图。

我还有其他一些变体...您也可以使用包装器之类的东西,仅支持少量功能:

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]
}

用法与Map有所不同,但仍然非常简单:

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

如果类型不是从值派生的,那么您仍然可以使用上面的方法构造一个可能满足您需求的解决方案。

答案 2 :(得分:1)

如果您希望地图在初始化后不可变,则可以执行以下操作:

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()

并按以下方式使用它:

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
}