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



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

我知道首选的方法在Scala编程中有讨论,但我目前还没有读过这本书。

还有一个免费的第一版PinS也讨论了这个主题。然而,我认为最好的来源是Odersky在Java中讨论平等的这篇文章。PinS中的讨论是本文的缩写。

经过大量研究,我找不到Scala类(不是case类,因为它是编译器自动生成的,不应该被重写)的equalshashCode模式的基本正确实现的答案。我确实发现一个2.10(过时)的宏正在勇敢地尝试解决这个问题。

我最终结合了";有效Java,第二版";(Joshua Bloch),并且在本文中";如何用Java编写一个相等方法";(由Martin Odersky、Lex Spoon和Bill Venners撰写),并创建了一个标准的默认模式,我现在用它来为我的Scala类实现equalshashCode

equals模式的主要目标是最大限度地减少执行实际比较所需的数量,以获得有效和明确的truefalse

此外,当equals方法被重写时,hashCode方法应该总是被重写并重新实现(再次参见"Effective Java,2nd Edition"(约书亚·布洛赫))。因此,我加入了hashCode方法";图案";在下面的代码中,它还包含了关于在实际实现中使用CCD_ 13而不是CCD_。

值得一提的是,只有当祖先已经重写了super.equalssuper.hashCode时,才必须调用它们。如果没有,则必须不调用super.*作为java.lang.Object中的默认实现(equals比较同一类实例,hashCode很可能将对象的内存地址转换为整数),这两者都将破坏为现在被重写的方法指定的CCD_ 21和CCD_。

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. 同一实例短路-为true测试(this eq person)可确保不会进行进一步的(昂贵的)比较,因为它实际上是同一实例。由于eq方法在AnyRef上可用,而不是在Any(that类型)上可用,因此此测试需要在模式匹配内。由于AnyRefPerson的祖先,该技术通过对后代Person进行类型验证来同时进行两次类型验证,这意味着对其所有祖先进行自动类型验证,包括AnyRef,这是eq检查所必需的。虽然此测试在技术上是可选的,但仍强烈建议使用
  3. 检查thatcanEqual-很容易把它倒过来,这是不正确的。以this为参数,在that实例上执行canEqual的检查是至关重要的。虽然它对模式匹配来说可能是多余的(假设我们进入这行代码,that必须是Person实例),但我们仍然必须进行方法调用,因为我们不能假设thatPerson的等价兼容后代(Person的所有后代都将成功地作为Person进行模式匹配)。如果该类被标记为final,则此测试是可选的,可以安全地删除。否则,它是必需的
  4. 检查hashCode短路-虽然不充分也不需要,但如果此hashCode测试为false,则无需执行所有值级检查(项目5)。如果此测试是true,则实际上需要逐字段检查。此测试是可选的,如果hashCode值没有缓存,则可能会被排除。每个字段的相等性检查的总成本足够低
  5. 每字段相等性检查-即使提供了hashCode测试并成功,仍必须检查所有字段级别的值。这是因为,尽管这是极不可能的,但两个不同的实例仍然有可能生成完全相同的hashCode值,并且在字段级别上仍然不等效。还必须调用父级的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. Scala编程,第一版-错过了上面的1、2和4
  2. 阿尔文·亚历山大的斯卡拉食谱-错过了1、2和4
  3. Scala编程代码示例-在生成类的hashCode覆盖和实现时,错误地在类字段上使用.hashCode而不是.##。参见Tree3.scala

是的,在Java和Scala中,重写equalshashCode都是一项dauting任务。我建议不要使用equals,而是使用类型类(Eq/Eql等)。它更安全(比较不相关的类型时会出现编译器错误),更容易实现(没有重写和类检查),也更灵活(可以编写与数据类分离的类型类实例)。Dotty使用了"多重平等"的概念,它在捕捉equals的一些明显不正确的用法和严格的平等检查la Haskell之间提供了一个选择。

最新更新