Какова стандартная идиома для реализации методов equals
и hashCode
в Scala?
Я знаю, что предпочтительный подход обсуждается в Программирование в Scala, но в настоящее время у меня нет доступа к книге.
Какова стандартная идиома для реализации методов equals
и hashCode
в Scala?
Я знаю, что предпочтительный подход обсуждается в Программирование в Scala, но в настоящее время у меня нет доступа к книге.
Там есть бесплатное 1-е издание PinS, которое также обсуждает эту тему. Тем не менее, я считаю, что лучшим источником является в этой статье Одерского, обсуждая равенство в Java. Обсуждение в PinS - это iirc, сокращенная версия этой статьи.
Проведя немало исследований, я не смог найти ответ с необходимой и правильной реализацией шаблона equals
и hashCode
для класса Scala (не для класса case, поскольку он генерируется автоматически компилятором и не должен быть переопределен). Я нашел макрос 2.10 (несвежий), который решительно пытается решить эту проблему.
В итоге я соединил шаблоны, рекомендованные в "Эффективной Java, 2-е издание" (Джошуа Блох) и в этой статье "Как написать метод равенства в Java" (Мартин Одерский, Лекс Спун и Билл Веннерс), и создал стандартный шаблон по умолчанию, который я сейчас использую для реализации equals
и hashCode
для моих классов Scala.
Основная цель шаблона equals
- минимизировать количество фактических сравнений, необходимых для выполнения, чтобы получить действительное и окончательное значение true
или false
.
Кроме того, метод hashCode
должен ВСЕГДА быть переопределен и повторно реализован, когда переопределен метод equals
(снова см. "Эффективная Java, 2-е издание" (автор Джошуа Блох)). Следовательно, мое включение "шаблона" метода hashCode
в приведенный ниже код, который также включает в себя критический совет по использованию ##
вместо hashCode
в реальной реализации.
Стоит отметить, что каждый из super.equals
и super.hashCode
должен вызываться только в том случае, если предок его уже переопределил. Если нет, то крайне важно НЕ вызывать super.*
Как реализацию по умолчанию в java.lang.Object
( equals
для сравнения для того же экземпляра класса, а hashCode
скорее всего, преобразует адрес памяти объекта в целое число), оба из которых нарушит указанный контракт 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
формализацию метода canEqual
. Хотя расширение является технически необязательным, оно по-прежнему настоятельно рекомендуется.(this eq person)
для true
гарантирует дальнейших (дорогих) сравнений, поскольку это буквально тот же самый экземпляр. Этот тест должен быть внутри сопоставления с шаблоном, так как метод eq
доступен в AnyRef
, а не в Any
(тип that
). А поскольку AnyRef
является предком Person
, этот метод выполняет две одновременные проверки типов посредством проверки типа потомка Person
, что подразумевает автоматическую проверку типа всех его предков, включая AnyRef
, что требуется для проверки eq
. Хотя этот тест технически необязателен, он по-прежнему настоятельно рекомендуется.that
canEqual
- очень легко получить это назад, что НЕПРАВИЛЬНО. Крайне важно, чтобы проверка canEqual
была выполнена в that
экземпляре с this
параметром. И хотя это может показаться излишним для сопоставления с шаблоном (учитывая, что мы попадаем в эту строку кода, that
должен быть экземпляр Person
), мы все равно должны сделать вызов метода, поскольку не можем предположить, that
это равнозначно совместимый потомок Person
(все потомки Person
будут успешно сопоставлять образец как Person
). Если класс помечен как final
, этот тест необязателен и может быть безопасно удален. В противном случае это требуется.hashCode
- хотя этот тест hashCode
не является достаточным и не обязательным, он false
, но устраняет необходимость выполнять все проверки на уровне значений (элемент 5). Если этот тест true
, то проверка по полю фактически требуется. Этот тест не является обязательным и может быть исключен, если значение hashCode не кэшировано и общая стоимость проверок на равенство полей достаточно мала.hashCode
предоставлен и успешно выполнен, все значения уровня поля все равно должны быть проверены. Это связано с тем, что, хотя это крайне маловероятно, для двух разных экземпляров остается возможным сгенерировать одно и то же значение hashCode
, и все же оно не может быть фактически эквивалентным на уровне поля. Родительский equals
также должен быть вызван, чтобы гарантировать, что любое дополнительное поле, определенное в предках, также проверено.case _ =>
- это фактически достижение двух разных эффектов. Во-первых, сопоставление с шаблоном Scala гарантирует, что значение null
здесь правильно направлено, поэтому null
не должен появляться где-либо внутри нашего чистого кода Scala. Во-вторых, сопоставление с шаблоном гарантирует, что бы that
ни было, это не экземпляр Person
или один из его потомков.super.equals
и super.hashCode
немного сложно - если предок уже переопределил оба (и никогда не должен быть), крайне важно, чтобы вы включили 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.##
И последнее замечание: в моих исследованиях я не мог поверить, сколько существует ошибочных реализаций этого шаблона. Это, безусловно, все еще область, где трудно получить правильные детали:
.hashCode
вместо .##
в полях классов при генерации переопределения и реализации hashCode
класса. Смотрите Tree3.scala