Какова стандартная идиома для реализации equals и hashCode в Scala? - PullRequest
28 голосов
/ 10 сентября 2011

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

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

Ответы [ 2 ]

19 голосов
/ 15 сентября 2011

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

5 голосов
/ 08 июня 2019

Проведя немало исследований, я не смог найти ответ с необходимой и правильной реализацией шаблона equals и hashCode для класса Scala (не для класса case, поскольку он генерируется автоматически компилятором ине должно быть переопределено).Я действительно нашел макрос 2.10 (устаревший) , который доблестно пытался решить эту проблему.

В итоге я соединил шаблоны, рекомендованные в «Эффективная Java, 2-е издание» (ДжошуаBloch) и в этой статье "Как написать метод равенства в Java" (автор Martin Martin Odersky, Lex Spoon и Bill Venners) и создали стандартный шаблон по умолчанию, который я сейчас использую для реализации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, полностью реализуется.Несмотря на то, что расширение является технически необязательным, оно настоятельно рекомендуется.
  2. То же самое короткое замыкание экземпляра - тестирование (this eq person) для true не гарантирует дальнейших (дорогих) сравнений, поскольку это буквально тот же экземпляр.Этот тест должен быть внутри сопоставления с шаблоном, так как метод eq доступен для AnyRef, а не для Any (тип that).И поскольку AnyRef является предком Person, этот метод выполняет две одновременные проверки типов посредством проверки типа потомка Person, что подразумевает автоматическую проверку типа всех его предков, включая AnyRef, что требуетсядля проверки eq.Хотя этот тест технически необязателен, он по-прежнему настоятельно рекомендуется.
  3. Проверьте that s 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. Кулинарная книга Алвина Александра Скала - Пропущено 1, 2 и 4.
  3. Примеры кода для программирования в Scala - Неправильно используется .hashCode вместо .## в полях классов при генерации переопределения и реализации класса hashCode. Смотрите Tree3.scala
...