【问题标题】:What is the standard idiom for implementing equals and hashCode in Scala?在 Scala 中实现 equals 和 hashCode 的标准习惯用法是什么?
【发布时间】:2011-09-10 09:48:18
【问题描述】:

在 Scala 中实现 equalshashCode 方法的标准习惯用法是什么?

我知道在 Programming in Scala 中讨论了首选方法,但我目前无法访问这本书。

【问题讨论】:

标签: scala equals hashcode


【解决方案1】:

还有一个免费的第 1 版 PinS 也讨论了这个主题。但是,我认为最佳 来源是 Odersky 讨论 Java 中的平等的 this article。 PinS 中的讨论,iirc,是本文的缩略版。

【讨论】:

  • 有趣的是,在文章的末尾有一条说明表明该文章是基于 PinS 中的对象平等章节。这本书有而文章没有的东西是a summary of the steps to go through when writing an equals method。因此,我倾向于喜欢这本书的版本。很高兴在 Java 和 Scala 中看到相同的材料。
  • 在此处包含文章的相关部分会很棒。另外,它处理 Java,而不是 Scala。 (当然,很多问题都是一样的。)
  • @Michael 请查看我的回答,其中引用、重新实现和更正了文章中的详细信息。 stackoverflow.com/a/56509518/501113
【解决方案2】:

在做了相当多的研究之后,我找不到一个答案,它可以为 Scala 类(不是案例类,因为它是编译器自动生成的,并且should not be overridden)。我确实找到了一个 2.10 (stale) macro 勇敢地尝试解决这个问题。

我最终结合了"Effective Java, 2nd Edition" (by Joshua Bloch) 和本文中的"How to Write an Equality Method in Java" (by Martin Odersky, Lex Spoon, and Bill Venners) 中推荐的模式,并创建了一个标准的默认模式,我现在使用它来为我的Scala 类实现equalshashCode

equals 模式的主要目标是最大程度地减少执行实际比较所需的次数,以达到有效且确定的truefalse

此外,hashCode 方法应始终被覆盖并在 equals 方法被覆盖时重新实现(再次参见 "Effective Java, 2nd Edition" (by Joshua Bloch))。因此,我在下面的代码中包含了hashCode 方法“模式”,在实际实现中也包含了critical advice about using ## instead of hashCode

值得一提的是,super.equalssuper.hashCode 中的每一个都必须在祖先已经覆盖它时才被调用。如果不是,则必须不调用super.* 作为java.lang.Object 中的默认实现(equals compares for the same class instancehashCode most likely converts the memory address of the object into an integer),这两者都将破坏指定的equalshashCode 合约,用于现在被覆盖的方法.

class Person(val name: String, val age: Int) extends Equals {
  override def canEqual(that: Any): Boolean =
    that.isInstanceOf[Person]

  //Intentionally avoiding the call to super.equals because no ancestor has overridden equals (see note 7 below)
  override def equals(that: Any): Boolean =
    that match {
      case person: Person =>
        (     (this eq person)                     //optional, but highly recommended sans very specific knowledge about this exact class implementation
          ||  (     person.canEqual(this)          //optional only if this class is marked final
                &&  (hashCode == person.hashCode)  //optional, exceptionally execution efficient if hashCode is cached, at an obvious space inefficiency tradeoff
                &&  (     (name == person.name)
                      &&  (age == person.age)
                    )
              )
        )
      case _ =>
        false
    }

  //Intentionally avoiding the call to super.hashCode because no ancestor has overridden hashCode (see note 7 below)
  override def hashCode(): Int =
    31 * (
      name.##
    ) + age.##
}

代码有许多非常重要的细微差别:

  1. 扩展scala.Equals - 确保equals 惯用模式(包括canEqual 方法形式化)得到全面实施。虽然扩展它在技术上是可选的,但仍然强烈推荐。
  2. 相同实例短路 - 测试 (this eq person)true 可确保不再进行(昂贵的)比较,因为它实际上是相同的实例。此测试必须在模式匹配内,因为eq 方法在AnyRef 上可用,而不是在Anythat 的类型)上可用。由于AnyRefPerson 的祖先,因此该技术通过对后代Person 进行类型验证来同时进行两次类型验证,这意味着对其所有祖先(包括AnyRef)进行自动类型验证,这是必需的对于eq 检查。虽然此测试在技术上是可选的,但仍强烈推荐。
  3. 检查thatcanEqual - 很容易将其倒退,这是不正确的。在that 实例上执行canEqual 的检查至关重要,this 作为参数提供。虽然它对于模式匹配似乎是多余的(鉴于我们得到这行代码,that 必须是一个 Person 实例),我们仍然必须进行方法调用,因为我们不能假设 that 是一个等于- Person 的兼容后代(Person 的所有后代将成功地模式匹配为 Person)。如果该类标记为final,则此测试是可选的并且可以安全地删除。否则,它是必需的。
  4. 检查hashCode 短路 - 虽然不充分也不需要,但如果此hashCode 测试为false,则无需执行所有值级别检查(第 5 项)。如果这个测试是true,那么实际上需要逐个字段检查。此测试是可选的,如果 hashCode 值未缓存并且每个字段相等检查的总成本足够低,则可能会被排除在外。
  5. 每字段相等性检查 - 即使提供了 hashCode 测试并成功,仍然必须检查所有字段级别的值。这是因为,尽管可能性极小,it remains possible for two different instances to generate the exact same hashCode value, and still not be actually equivalent at the field level。还必须调用父级的 equals 以确保在父级中定义的任何其他字段也被测试。
  6. 模式匹配case _ => - 这实际上实现了两种不同的效果。首先,Scala 模式匹配保证null 在此处正确路由,因此null 不必出现在我们纯Scala 代码中的任何位置。其次,模式匹配保证无论that 是什么,它都不是Person 或其后代之一的实例。
  7. 何时调用 super.equalssuper.hashCode 有点棘手 - 如果祖先已经覆盖了这两个(不应该是其中任何一个),则必须将 super.* 合并到您自己的覆盖实现中。如果一个祖先没有覆盖两者,那么你覆盖的实现必须避免调用super.*。上面的Person 代码示例显示了没有祖先 覆盖两者的情况。因此,调用每个 super.* 方法调用将错误地一直落入默认的 java.lang.Object.* 实现,这将使 equalshashCode 的假定组合合同无效。

这是基于super.equals 的代码,仅当至少有一个祖先已明确覆盖equals 时才使用。

override def equals(that: Any): Boolean =
  ...
    case person: Person =>
      ( ...
                //WARNING: including the next line ASSUMES at least one ancestor has already overridden equals; i.e. that this does not end up invoking java.lang.Object.equals
                &&  (     super.equals(person)     //incorporate checking ancestor(s)' fields
                      &&  (name == person.name)
                      &&  (age == person.age)
                )
            ...
      )
    ...

这是基于super.hashCode 的代码,仅当至少有一个祖先已明确覆盖hashCode 时才使用。

override def hashCode(): Int =
  31 * (
    31 * (
      //WARNING: including the next line ASSUMES at least one ancestor has already overridden hashCode; i.e. that this does not end up invoking java.lang.Object.hashCode
      super.hashCode  //incorporate adding ancestor(s)' hashCode (and thereby, their fields)
    ) + name.##
  ) + age.##

最后一点:在我为此进行研究时,我无法相信这种模式存在多少错误的实现。显然,这仍然是一个难以准确把握细节的领域:

  1. Programming in Scala, First Edition - 错过了上面的 1、2 和 4。
  2. Alvin Alexander's Scala Cookbook - 错过了 1、2 和 4。
  3. Code Examples for Programming in Scala - 在生成类的 hashCode 覆盖和实现时,在类字段上错误地使用 .hashCode 而不是 .##。参见 Tree3.scala

【讨论】:

  • 你为什么不在这里使用一个案例类?
  • 在 hashCode 的实现中不要使用 .hashCode。采用 ##。它相当于 == 并解决了合作平等问题。
  • 如果某些东西太复杂而不能成为案例类,但仍需要被视为“值对象”(即具有等于、hashCode),那么可能是时候分解一些部分了。将自己置于必须手动编写这种机械(并且正如您指出的那样容易出错)代码的情况下,您似乎没有充分利用该语言。
  • @chaotic3quilibrium 是的
  • @Thilo 我同意 Scala 案例类很棒。但是,我认为您对 Scala 软件工程师必须解决问题的众多不同上下文做出了太多假设。我不能告诉其他软件工程师他们必须使用案例类来解决他们的问题。我没有也不会有足够的上下文来自信地断言“正确”的解决方案。我所做的唯一假设是有时(甚至可能很少),有人需要实现这个模式(包括我),所以明确地捕获这个模式仍然很有帮助。
【解决方案3】:

是的,覆盖 equalshashCode 在 Java 和 Scala 中都是一项艰巨的任务。我建议根本不要使用equals,而是使用类型类(Eq/Eql 等)。它更加类型安全(比较不相关类型时出现编译器错误),更易于实现(没有覆盖和类检查)并且更灵活(您可以编写与数据类分开的类型类实例)。 Dotty 使用了 "multiversal equality" 的概念,它提供了在捕捉 equals 的一些明显不正确的用法和严格相等检查(如 Haskell)之间的选择。

【讨论】:

  • 很好的建议。我同意并强烈建议该领域的任何人尽可能多地转向类型类解决方案。当然,问题在于该类是否在 Scala Collections 中使用。默认情况下,它们依赖于来自java.lang.ObjectequalshashCode 方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-19
  • 2014-04-06
相关资源
最近更新 更多