Подтвердить что ты не робот

Какова стандартная идиома для реализации equals и hashCode в Scala?

Какова стандартная идиома для реализации методов equals и hashCode в Scala?

Я знаю, что предпочтительный подход обсуждается в Программирование в Scala, но в настоящее время у меня нет доступа к книге.

4b9b3361

Ответ 1

Там есть бесплатное 1-е издание PinS, которое также обсуждает эту тему. Тем не менее, я считаю, что лучшим источником является в этой статье Одерского, обсуждая равенство в Java. Обсуждение в PinS - это iirc, сокращенная версия этой статьи.

Ответ 2

Проведя немало исследований, я не смог найти ответ с необходимой и правильной реализацией шаблона 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.##
}

В коде есть ряд нюансов, которые критически важны:

  1. Расширение scala.Equals - обеспечивает полную реализацию идиоматического шаблона equals, который включает в canEqual формализацию метода canEqual. Хотя расширение является технически необязательным, оно по-прежнему настоятельно рекомендуется.
  2. То же самое короткое замыкание экземпляра - Тестирование (this eq person) для true гарантирует дальнейших (дорогих) сравнений, поскольку это буквально тот же самый экземпляр. Этот тест должен быть внутри сопоставления с шаблоном, так как метод eq доступен в AnyRef, а не в Any (тип that). А поскольку AnyRef является предком Person, этот метод выполняет две одновременные проверки типов посредством проверки типа потомка Person, что подразумевает автоматическую проверку типа всех его предков, включая AnyRef, что требуется для проверки eq. Хотя этот тест технически необязателен, он по-прежнему настоятельно рекомендуется.
  3. Проверьте, that canEqual - очень легко получить это назад, что НЕПРАВИЛЬНО. Крайне важно, чтобы проверка canEqual была выполнена в that экземпляре с this параметром. И хотя это может показаться излишним для сопоставления с шаблоном (учитывая, что мы попадаем в эту строку кода, that должен быть экземпляр Person), мы все равно должны сделать вызов метода, поскольку не можем предположить, that это равнозначно совместимый потомок Person (все потомки Person будут успешно сопоставлять образец как Person). Если класс помечен как final, этот тест необязателен и может быть безопасно удален. В противном случае это требуется.
  4. Проверка короткого замыкания hashCode - хотя этот тест hashCode не является достаточным и не обязательным, он false, но устраняет необходимость выполнять все проверки на уровне значений (элемент 5). Если этот тест true, то проверка по полю фактически требуется. Этот тест не является обязательным и может быть исключен, если значение hashCode не кэшировано и общая стоимость проверок на равенство полей достаточно мала.
  5. Проверка на равенство полей - даже если тест hashCode предоставлен и успешно выполнен, все значения уровня поля все равно должны быть проверены. Это связано с тем, что, хотя это крайне маловероятно, для двух разных экземпляров остается возможным сгенерировать одно и то же значение hashCode, и все же оно не может быть фактически эквивалентным на уровне поля. Родительский equals также должен быть вызван, чтобы гарантировать, что любое дополнительное поле, определенное в предках, также проверено.
  6. Pattern match case _ => - это фактически достижение двух разных эффектов. Во-первых, сопоставление с шаблоном Scala гарантирует, что значение null здесь правильно направлено, поэтому null не должен появляться где-либо внутри нашего чистого кода Scala. Во-вторых, сопоставление с шаблоном гарантирует, что бы that ни было, это не экземпляр Person или один из его потомков.
  7. Когда вызывать каждый из 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.##

И последнее замечание: в моих исследованиях я не мог поверить, сколько существует ошибочных реализаций этого шаблона. Это, безусловно, все еще область, где трудно получить правильные детали:

  1. Программирование в Scala, первое издание - пропущено 1, 2 и 4 выше.
  2. Кулинарная книга Элвина Александра Scala - пропущено 1, 2 и 4.
  3. Примеры кода для программирования в Scala - неправильно использует .hashCode вместо .## в полях классов при генерации переопределения и реализации hashCode класса. Смотрите Tree3.scala