【问题标题】:Strange Kotlin behaviour using Data Classes in Maps在地图中使用数据类的奇怪 Kotlin 行为
【发布时间】:2022-01-14 06:02:14
【问题描述】:

我是 Kotlin 的新手,我正在努力理解它,我刚刚写了一个简单的例子,展示了如何使用带有地图的数据类有点棘手,因为在我看来数据类有一个奇怪的行为。 默认情况下,他们根据类的每个属性定义 hashCode()。但他们没有定义默认的 equals() 方法。 这给我带来了很多困惑,因为我创建了一个以数据类为键的 HashMap,但我没有覆盖 hashCode() 和 equals()。我的数据类有一个 MutableList 成员。当我在地图中放置一个元素时,只要我没有向 MutableList 添加元素,我就会使用 map.get(dataObject) 检索它。之后,即使数据对象还是一样的,我使用map.keys发现它(map.keys.indexOf(dataObject) 有效),map.get(dataObject) 失败,由于hashCode()。

我可以使用普通类或添加 hashCode() 和 equals() 来修复它,从 hashCode() 中删除 MutableList,但我想知道是否由于默认行为,覆盖了 hashCode() 和 equals()数据类应该是“强制性的”,否则将它们与地图一起使用可能会导致错误。

我还能做些什么来避免这个问题吗?

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

【问题讨论】:

  • 数据类确实定义了一个 equals() 方法
  • 顺便说一下,你可以直接写data class Player(val name: String, var cards: MutableList&lt;Card&gt; = mutableListOf()),而不是Player 中的第二个构造函数。我相信这具有相同的行为

标签: kotlin hashmap hashcode data-class


【解决方案1】:

默认情况下,它们根据类的每个属性定义 hashCode()。但是他们没有定义默认的 equals() 方法

这是不正确的。数据类根据数据类的主构造函数中声明的属性一致地生成equals()hashCode()(顺便说一句,toString() 也是如此)。

这是equalshashCodePlayer 类的反编译代码:

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

您的问题是您在主构造函数中声明了您的 cards 可变列表,因此它是生成的 equalshashCode 的一部分。

解决方案是将这个 cards 属性移到类的主体中(因为它不是玩家“核心数据”的一部分,而是状态的一部分):

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

这样,生成的equals/hashCode 对将仅基于name 属性。

另一个选择显然是手动覆盖equalshashCode 以仅考虑name,但这很乏味而且不是很惯用。

我想知道,由于默认行为,覆盖 hashCode() 和 equals() 是否应该是数据类的“强制性”,否则将它们与 Maps 一起使用会导致错误。

我认为您误诊了默认行为。所以我会说相反,覆盖equals/hashCode 对于数据类来说实际上不是很惯用,通常应该避免。

只要主构造函数中的数据不可变,在地图中使用数据类通常是安全的。


旁注

  • 您真的不应该将var 与可变集合混用。它创建了两种更改集合的方法,这是非常出乎意料且容易出错的。您应该改为使用val MutableListvar List,因此您只能通过突变更改列表,或仅通过赋值更改列表,但不能同时使用两者。

  • 如果要将新值插入到映射中,则不应使用getOrDefault + 将值分配给键。而是直接使用getOrPut,这样会插入默认值而不需要额外的工作。

  • 为什么你们在PlayerMap&lt;Player, List&lt;Card&gt;&gt; 上都使用cards 属性?看起来您现在有 2 个状态可以独立更改,因为这些卡列表是独立的。

【讨论】:

  • 谢谢,这解释了一些事情:) > 只要主构造函数中的数据不可变。这是我错过的关键点。主构造函数参数是一种主键。
  • > 为什么你在 Player 和 Map> 上都使用卡片属性?这只是我写的一个例子来说明我遇到的问题。两组卡是独立的。 Player.cards 代表玩家手中的牌,而地图代表他在具有此概念的特定游戏中赢得的牌(其他游戏没有),在我看来 Data 类无法扩展。但这只是熟悉 Kotlin 结构和语义的代码。
  • 有道理,谢谢你的澄清。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-23
  • 2013-08-10
相关资源
最近更新 更多