3
votes

Comment créer une carte avec des génériques dans Kotlin?

J'ai besoin de créer une carte où les clés sont des classes et les valeurs sont des objets de classes appropriées.

Comme:

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

Je veux utiliser des génériques pour éviter ' entrées invalides, comme Int :: class à "Ten"

Comment puis-je l'implémenter?


3 commentaires

Je pense que ce n'est pas possible, mais j'aimerais avoir tort. :-)


Je suis sûr que vous pourriez écrire une classe de carte spécialisée qui pourrait fournir ce genre de sécurité de type. Mais je n'en connais aucun. Et je ne sais pas si cela pourrait implémenter l'interface Map existante.


Pour les mettre dans une liste, vous perdrez les informations de type. À la toute fin, ce sera Map .


3 Réponses :


2
votes

Votre exemple utilisant des génériques ne décrit pas réellement votre objectif pour cette carte. Les génériques de l'interface Map ne sont pas capables de décrire le type de fonctionnalité que vous souhaitez. Les types de clés et les valeurs devraient encapsuler chaque clé et valeur que vous mettez dans cette carte, c'est donc possible:

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

Pour obtenir la sécurité de type autour de clés et valeurs particulières dans cette carte, vous auriez à faire une partie de votre propre travail pour envelopper une telle carte générale. Voici un exemple:

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

Je suis sûr que vous pouvez comprendre comment implémenter les autres méthodes générales d'une carte à partir de cela.


4 commentaires

+1. En raison de la façon dont Kotlin déduit les types dans les génériques, il est possible d'avoir des types liés, mais pas égaux, pour T . Exemple: put (Number :: class, 3) ou même put (Any :: class, 3) . Vous pouvez utiliser une fonction réifiée qui infère la classe à partir de l'argument (donc put (3) serait put (3) ) , mais cela peut toujours être mal utilisé si vous spécifiez explicitement l'argument type. Finalement, vous ne pouvez vérifier l'égalité réelle qu'au moment de l'exécution, par exemple assert (clé == valeur :: classe) .


De plus, pour les classes de type map, je recommande de surcharger les opérateurs set et get , pour activer la syntaxe map [key] et map [clé] = valeur .


@TheOperator belle suggestion sur la surcharge de l'opérateur


Je pense que si vous incluez le constructeur de constructeur de fonction d'extension de constructeur @xinaiz à partir de sa réponse, vous aurez une classe assez complète qui fait ce que vous voulez.



1
votes

Si vous voulez que votre carte soit immuable après l'initialisation, vous pouvez faire ceci:

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
}

Et l'utiliser de la manière suivante:

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


0 commentaires

4
votes

Je ne sais pas si j'obtiens ce que vous voulez vraiment accomplir. N'oubliez pas que les génériques sont effacés au moment de l'exécution donc à la fin vous n'aurez qu'un Map , Any> (plus correctement: Map code>). Néanmoins, le moyen le plus simple est probablement de vous en tenir à ce que vous savez déjà. Vous avez déjà montré une méthode pratique ( à ) pour créer un Pair qui est ensuite passé au mapOf , alors pourquoi ne pas simplement utiliser une nouvelle fonction qui répond à vos exigences, par exemple

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

Vous pouvez donc utiliser:

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

Bien sûr, de cette façon, vous pouvez toujours ajouter d'autres constellation dans cette carte. Si vous voulez surmonter cela, vous avez encore quelques options différentes disponibles:

Que diriez-vous de créer une fonction supplémentaire typedMapOf , par exemple:

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 }


0 commentaires