web-dev-qa-db-fra.com

Comportement de Kotlin étrange à l'aide de classes de données dans des cartes

Je suis nouveau à Kotlin et j'essaie de la comprendre, je viens de dire un exemple simple qui montre comment utiliser des classes de données avec des cartes est un peu délicate, car il me semble que les classes de données ont un comportement étrange. Par défaut, ils définissent le hashcode () sur la base de toutes les biens de la classe. Mais ils ne définissent pas une méthode égale () par défaut. Cela m'a causé beaucoup de confusion car j'ai créé un haschmap avec une classe de données comme clé, mais je n'ai pas remplacé par hashcode () et égale (). Ma classe de données a un membre mutablociste. Lorsque je mettais un élément sur la carte, je l'ai récupéré à l'aide de Map.get (DataObject) tant que je n'ai pas ajouté d'élément à l'agent mutable. Après cela, même si l'objet de données était toujours identique, et je l'ai trouvé à l'aide de Carte.Keys (Map.Keys.indexof), carap.get (DataObject) a échoué, en raison de la hausse ().

Je peux le réparer à l'aide d'une classe normale ou d'ajout de hashcode () et d'équivalents (), en supprimant l'mutableliste de HASHCODE (), mais je vous demande si, en raison du comportement par défaut, le hachemode () et l'équivalent (). obligatoire "avec des classes de données car sinon les utiliser avec des cartes peut entraîner des erreurs.

Y a-t-il autre chose que je puisse faire pour éviter ce problème?

    package cards
    
    data class Player(val name: String, var cards: MutableList<Card>) {
        constructor(name: String): this(name, mutableListOf())
    
    //I don't need to define equals, so pointers are checked. But if I don't override hashCode, as it's based
    //on every property, the hashCode is calculated considering the content of the MutableList!
    //    override fun hashCode(): Int {
    //        return name.hashCode()
    //    }
    
    }
    
    data class Card(val name: String, val suite: String)
    
    class Game(val players: List<Player>) {
    
        val cardMap: MutableMap<Player, MutableList<Card>> = mutableMapOf()
    
        fun putIntoMapAndGiveCards() {
            val newCards = cardMap.getOrDefault(players[0], mutableListOf())
            newCards.add(Card(name = "Four", suite = "Clubs"))
            cardMap[players[0]] = newCards
    
            //This changes the default hashCode - I can use data classes in a list, but not in a map, because maps are
            //based on it.
            players[0].cards.add(Card(name = "Five", suite = "Clubs"))
        }
    
        fun getFromMap(): MutableList<Card>? {
            val player = players[0]
            assert(player != null, { "Player from list failure" })
    
            val indexOfPlayer = cardMap.keys.indexOf(player)
            assert(indexOfPlayer == 0, { "Player is in the map" })
    
            //Without overriding hashCode, cards is null!
            val cards = cardMap.get(players[0])
            assert(cards != null, { "Cards from map failure" })
            return cards
        }
    
    }
    
    
    fun main() {
        val player1 = Player(name = "John")
        val game = Game(mutableListOf(player1))
        game.putIntoMapAndGiveCards()
    
        game.getFromMap()
            ?: throw Exception( """Map.get() failure because Player is a data class.
            | A data class by default builds its hashCode with every property. As it contains a MutableList, 
            |   the hashCode changes when I add elements to the list. This means that I can't find the element using get()
        """.trimMargin())
    
        println("Test finished!")
    }
1
David Obber

Par défaut, ils définissent le hashcode () sur la base de toutes les biens de la classe. Mais ils ne définissent pas une méthode égale par défaut ()

Ce n'est pas correct. Les classes de données génèrent à la fois equals() et hashCode() _ _ Basé sur la base des propriétés déclarées dans le constructeur principal de la classe de données (même chose pour toString() BTW).

Voici le code décompilé pour equals et hashCode de votre classe Player classe:

   public int hashCode() {
      String var10000 = this.name;
      int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
      List var10001 = this.cards;
      return var1 + (var10001 != null ? var10001.hashCode() : 0);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Player) {
            Player var2 = (Player)var1;
            if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.cards, var2.cards)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }

Votre problème est que vous déclarez votre liste cards une liste mutable dans le constructeur principal, elle fait donc partie du généré equals et hashCode.

La solution consiste à déplacer cette propriété cards propriété sur le corps de votre classe (car elle ne fait pas partie des "données de base" du joueur, mais plutôt une partie de l'état):

data class Player(val name: String) {
    val cards: MutableList<Card> = mutableListOf()
}

De cette façon, la paire générée equals/hashCode sera basée uniquement sur la propriété name.

Une autre option consiste évidemment à remplacer à la fois equals et hashCode manuellement pour ne prendre que le compte name en compte, mais c'est fastidieux et pas très idiomatique.

Je me demande si, en raison du comportement par défaut, le hachage remplissant () et les égaux () devraient être "obligatoires" avec des classes de données car sinon les utiliser avec des cartes peuvent entraîner des erreurs.

Je pense que vous avez mal diagnostiqué le comportement par défaut. Donc, je dirais que je dirais sur le contraire remplacer equals/hashCode est en fait pas très idiomatique pour les classes de données et doit être évitée en général.

L'utilisation de classes de données est généralement sûre dans les cartes, à condition que les données du constructeur primaire ne soient pas mutables.


Notes latérales

  • vous ne devez vraiment pas mélanger var avec des collections mutables. Cela crée 2 façons de changer la collection, qui est assez inattendue et évacuée. Vous devriez plutôt utiliser un val MutableList ou un var List, vous ne pouvez donc modifier que la liste via la mutation ou seulement la modifier via une affectation, mais pas les deux.

  • si vous souhaitez insérer la nouvelle valeur dans la carte, vous ne devez pas utiliser getOrDefault + Attribuer la valeur à la touche. Utilisez plutôt getOrPut directement, la valeur par défaut sera insérée sans travail supplémentaire.

  • pourquoi utilisez-vous tous les deux une propriété cards sur le Player et un Map<Player, List<Card>>? On dirait que vous avez 2 états qui peuvent changer de manière indépendante, car ces listes de cartes sont indépendantes.

6
Joffrey