在Scala中实现equals和hashCode的标准习惯用法是什么?

时间:2011-09-10 09:48:18

标签: scala equals hashcode

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

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

3 个答案:

答案 0 :(得分:16)

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

答案 1 :(得分:5)

经过大量研究后,我找不到针对Scala类(而不是案例类)的equalshashCode模式的基本正确实现的答案。自动编译器生成,因此不应覆盖)。我确实发现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)中推荐的模式,并创建了一个标准的默认模式,我现在使用它来实现equalshashCode我的Scala课程。

equals模式的主要目标是最大程度地减少为获得有效且确定的truefalse而执行的实际比较次数。

此外,hashCode方法应始终被覆盖,并在equals方法被覆盖时重新实现(再次参见"Effective Java, 2nd Edition" (by Joshua Bloch))。因此,我在下面的代码中加入了hashCode方法“模式”,该代码在实际实现中还结合了critical advice关于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进行类型验证来进行两个同时的类型验证,这意味着将对其所有祖先(包括{ {1}},这是AnyRef检查所必需的。尽管此测试在技术上是可选的,但仍强烈建议使用。
  3. 检查eq的{​​{1}}-很容易使此倒退,这是不正确的。使用提供的that作为参数,在canEqual实例上执行canEqual的检查至关重要。尽管对于模式匹配来说似乎是多余的(鉴于我们已经到了这一行代码,that必须是一个this实例),但是我们仍然必须进行方法调用,因为我们不能假设{{1 }}是that的等值兼容后代(Person的所有后代将成功地像that那样进行模式匹配)。如果该类标记为Person,则此测试为可选测试,可以安全删除。否则,它是必需的。
  4. 检查Person短路-尽管还不足够,也不是必需的,但是如果此Person测试为final,则无需执行所有值级别检查(项目5)。如果此测试为hashCode,则实际上需要逐字段检查。此测试是可选的,如果未缓存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非常不可能。还必须调用父级的false,以确保也测试了祖先中定义的任何其他字段。
  6. 模式匹配true-实际上实现了两种不同的效果。首先,Scala模式匹配确保hashCode在此处正确路由,因此equals不必出现在纯Scala代码中的任何位置。其次,模式匹配可以保证无论case _ =>是什么,它都不是null的实例或其后代之一。
  7. 何时调用nullthat中的每一个都有些棘手-如果祖先已经覆盖了这两个(永远都不是),则必须合并Person在您自己的替代实现中。而且,如果祖先没有覆盖这两个对象,那么您覆盖的实现必须避免调用super.equals。上面的super.hashCode代码示例显示了没有 ancestor 覆盖两者的情况。因此,每次调用super.*方法调用都会错误地落到默认的super.*实现中,这将使Personsuper.*的假定组合合同无效。

这是基于java.lang.Object.*的代码,仅在至少有一个祖先已显式重写equals的情况下才能使用。

hashCode

这是基于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)
                )
            ...
      )
    ...

最后一点:在我为此做的研究中,我不敢相信存在这种模式的许多错误实现。显然,仍然是很难正确获取细节的地方:

  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-在生成类的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.## 。参见Tree3.scala

答案 2 :(得分:0)

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