在Scala中实现equals
和hashCode
方法的标准习惯用法是什么?
我知道 Programming in Scala 中讨论了首选方法,但我目前无法访问该书。
答案 0 :(得分:16)
还有一个免费的PinS第1版也讨论了这个主题。但是,我认为Odersky讨论Java中的相等性时,最佳源是this article。 PinS中的讨论是iirc,是本文的缩写版本。
答案 1 :(得分:5)
经过大量研究后,我找不到针对Scala类(而不是案例类)的equals
和hashCode
模式的基本正确实现的答案。自动编译器生成,因此不应覆盖)。我确实发现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)中推荐的模式,并创建了一个标准的默认模式,我现在使用它来实现equals
和hashCode
我的Scala课程。
equals
模式的主要目标是最大程度地减少为获得有效且确定的true
或false
而执行的实际比较次数。
此外,hashCode
方法应始终被覆盖,并在equals
方法被覆盖时重新实现(再次参见"Effective Java, 2nd Edition" (by Joshua Bloch))。因此,我在下面的代码中加入了hashCode
方法“模式”,该代码在实际实现中还结合了critical advice关于using ##
instead of hashCode
。
值得一提的是,super.equals
和super.hashCode
中的每一个仅在祖先已经覆盖它的情况下才必须被调用。如果不是,那么必须将super.*
作为java.lang.Object
中的默认实现(equals
compares for the same class instance和hashCode
most likely converts the memory address of the object into an integer),这两个都将破坏指定的equals
和hashCode
签约使用现在已覆盖的方法。
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.##
}
该代码具有许多至关重要的细微差别:
scala.Equals
-确保完全实现equals
惯用模式,其中包括canEqual
方法的形式化。扩展它在技术上是可选的,但仍然强烈建议使用。(this eq person)
与true
不会导致进一步的比较(昂贵)比较,因为它实际上是同一实例。由于eq
上的AnyRef
方法可用,而不是Any
(that
的类型)可用,因此此测试必须位于模式匹配之内。并且由于AnyRef
是Person
的祖先,因此该技术通过对后代Person
进行类型验证来进行两个同时的类型验证,这意味着将对其所有祖先(包括{ {1}},这是AnyRef
检查所必需的。尽管此测试在技术上是可选的,但仍强烈建议使用。eq
的{{1}}-很容易使此倒退,这是不正确的。使用提供的that
作为参数,在canEqual
实例上执行canEqual
的检查至关重要。尽管对于模式匹配来说似乎是多余的(鉴于我们已经到了这一行代码,that
必须是一个this
实例),但是我们仍然必须进行方法调用,因为我们不能假设{{1 }}是that
的等值兼容后代(Person
的所有后代将成功地像that
那样进行模式匹配)。如果该类标记为Person
,则此测试为可选测试,可以安全删除。否则,它是必需的。Person
短路-尽管还不足够,也不是必需的,但是如果此Person
测试为final
,则无需执行所有值级别检查(项目5)。如果此测试为hashCode
,则实际上需要逐字段检查。此测试是可选的,如果未缓存hashCode值并且每字段相等性检查的总成本足够低,则可以将该测试排除在外。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
,以确保也测试了祖先中定义的任何其他字段。true
-实际上实现了两种不同的效果。首先,Scala模式匹配确保hashCode
在此处正确路由,因此equals
不必出现在纯Scala代码中的任何位置。其次,模式匹配可以保证无论case _ =>
是什么,它都不是null
的实例或其后代之一。null
和that
中的每一个都有些棘手-如果祖先已经覆盖了这两个(永远都不是),则必须合并Person
在您自己的替代实现中。而且,如果祖先没有覆盖这两个对象,那么您覆盖的实现必须避免调用super.equals
。上面的super.hashCode
代码示例显示了没有 ancestor 覆盖两者的情况。因此,每次调用super.*
方法调用都会错误地落到默认的super.*
实现中,这将使Person
和super.*
的假定组合合同无效。这是基于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)
)
...
)
...
最后一点:在我为此做的研究中,我不敢相信存在这种模式的许多错误实现。显然,仍然是很难正确获取细节的地方:
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中,覆盖equals
和hashCode
都是一项艰巨的任务。我建议完全不要使用equals
,而应使用类型类(Eq
/ Eql
等)。它具有更好的类型安全性(比较不相关类型时出现编译器错误),更易于实现(没有覆盖和类检查)并且更灵活(可以将类型类实例与数据类分开编写)。 Dotty使用"multiversal equality"的概念,它在捕获equals
的一些明显不正确的用法和检查la Haskell的严格相等性之间提供了一种选择。