Переопределить GetHashCode для изменяемых объектов? - PullRequest
56 голосов
/ 17 мая 2009

Я прочитал около 10 различных вопросов о том, когда и как переопределить GetHashCode, но есть еще кое-что, что я не совсем понимаю. Большинство реализаций GetHashCode основаны на хэш-кодах полей объекта, но указывалось, что значение GetHashCode никогда не должно изменяться в течение срока службы объекта. Как это работает, если поля, на которых он основан, являются изменяемыми? Кроме того, что если я хочу, чтобы поиск по словарю и т. Д. Основывался на равенстве ссылок, а не на моем переопределенном Equals?

Я в первую очередь переопределяю Equals для простоты модульного тестирования моего кода сериализации, который я предполагаю, что сериализация и десериализация (к XML в моем случае) убивает ссылочное равенство, поэтому я хочу убедиться, что по крайней мере это правильно по равенству значений , Это плохая практика переопределять Equals в этом случае? В основном в большинстве исполняемого кода я хочу ссылочного равенства и всегда использую ==, и я не переопределяю это. Должен ли я просто создать новый метод ValueEquals или что-то вместо переопределения Equals? Раньше я предполагал, что фреймворк всегда использует ==, а не Equals для сравнения, и поэтому я подумал, что было бы безопасно переопределить Equals, так как мне показалось, что его целью было, если вы хотите иметь 2-е определение равенства, который отличается от оператора ==. Из чтения нескольких других вопросов, хотя кажется, что это не так.

EDIT:

Кажется, мои намерения были неясны, я имею в виду, что в 99% случаев я хочу получить простое ссылочное равенство, поведение по умолчанию, никаких сюрпризов. В очень редких случаях я хочу иметь равенство значений и явно запрашивать равенство значений, используя .Equals вместо ==.

Когда я делаю это, компилятор также рекомендует переопределить GetHashCode, и вот как возник этот вопрос. Казалось, что для GetHashCode существуют противоречивые цели применительно к изменяемым объектам:

  1. Если a.Equals(b), то a.GetHashCode() должно == b.GetHashCode().
  2. Значение a.GetHashCode() никогда не должно изменяться в течение срока службы a.

Это кажется естественным противоречием, когда изменяемый объект, потому что, если состояние объекта изменяется, мы ожидаем, что значение .Equals() изменится, что означает, что GetHashCode должен измениться, чтобы соответствовать изменению в .Equals(), но GetHashCode не должно меняться.

Почему, кажется, существует это противоречие? Разве эти рекомендации не предназначены для применения к изменчивым объектам? Возможно, предполагается, но, возможно, стоит упомянуть, что я имею в виду классы, а не структуры.

Разрешение:

Я отмечаю JaredPar как принятый, но в основном для взаимодействия с комментариями. Подводя итог, я понял из этого, что единственный способ достичь всех целей и избежать возможного странного поведения в крайних случаях - это переопределить только Equals и GetHashCode на основе неизменяемых полей или реализовать IEquatable. Похоже, что этот вид уменьшает полезность переопределения Equals для ссылочных типов, поскольку, как я видел, большинство ссылочных типов обычно не имеют неизменяемых полей, если они не хранятся в реляционной базе данных для идентификации их с их первичными ключами.

Ответы [ 5 ]

22 голосов
/ 17 мая 2009

Как это работает, если поля, на которых он основан, являются изменяемыми?

Это не означает, что хеш-код будет меняться при изменении объекта. Это проблема по всем причинам, перечисленным в статьях, которые вы прочитали. К сожалению, это тип проблемы, который обычно проявляется только в угловых случаях. Таким образом, разработчики склонны избегать плохого поведения.

Кроме того, что, если я хочу, чтобы поиск в словаре и т. Д. Основывался на равенстве ссылок, а не на моих переопределенных Equals? Пока вы реализуете интерфейс типа IEquatable<T>, это не должно быть проблемой. Большинство реализаций словаря выбирают средство сравнения равенства таким образом, чтобы использовать IEquatable<T> вместо Object.ReferenceEquals. Даже без IEquatable<T> большинство по умолчанию вызовет Object.Equals (), который затем войдет в вашу реализацию. В основном в большинстве выполняющегося кода я хочу ссылочного равенства и всегда использую ==, и я не переопределяю это.

Если вы ожидаете, что ваши объекты будут вести себя с равенством значений, вы должны переопределить == и! = Для обеспечения равенства значений для всех сравнений. Пользователи по-прежнему могут использовать Object.ReferenceEquals, если они действительно хотят равенство ссылок.

Раньше я предполагал, что каркас всегда использует ==, а не равно для сравнения вещей

То, что использует BCL, со временем немного изменилось. Теперь большинство случаев, в которых используется равенство, принимают экземпляр IEqualityComparer<T> и используют его для равенства. В тех случаях, когда один не указан, они будут использовать EqualityComparer<T>.Default, чтобы найти его. В худшем случае это по умолчанию вызовет Object.Equals

6 голосов
/ 17 мая 2009

Если у вас есть изменяемый объект, нет смысла переопределять метод GetHashCode, так как вы не можете его использовать. Например, он используется коллекциями Dictionary и HashSet для помещения каждого предмета в корзину. Если вы измените объект, когда он используется в качестве ключа в коллекции, хеш-код больше не будет соответствовать корзине, в которой находится объект, поэтому коллекция не будет работать должным образом, и вы больше никогда не сможете найти объект.

Если вы хотите, чтобы поиск не использовал метод класса GetHashCode или Equals, вы всегда можете предоставить собственную реализацию IEqualityComparer для использования вместо нее при создании Dictionary.

Метод Equals предназначен для равенства значений, поэтому его реализация не является ошибкой.

3 голосов
/ 17 мая 2009

Ух ты, на самом деле это несколько вопросов в одном :-). Итак, один за другим:

указывалось, что значение GetHashCode никогда не должно меняться в течение срока службы объекта. Как это работает, если поля, на которых он основан, являются изменяемыми?

Этот общий совет предназначен для случая, когда вы хотите использовать свой объект в качестве ключа в HashTable / словаре и т. Д. HashTables обычно требуют, чтобы хеш не менялся, потому что они используют его, чтобы решить, как сохранить и получить ключ. Если хэш изменится, HashTable, вероятно, больше не найдет ваш объект.

Для цитирования документов интерфейса Java Map :

Примечание: следует соблюдать особую осторожность, если в качестве ключей карты используются изменяемые объекты. Поведение карты не указывается, если значение объекта изменяется таким образом, что это влияет на сравнение с равными, в то время как объект является ключом на карте.

В общем, плохая идея использовать любой вид изменяемого объекта в качестве ключа в хэш-таблице: даже неясно, что должно произойти, если ключ изменяется после его добавления в хэш-таблицу. Должна ли хэш-таблица возвращать сохраненный объект через старый ключ, или через новый ключ, или через оба?

Итак, реальный совет: используйте только неизменяемые объекты в качестве ключей и убедитесь, что их хеш-код никогда не изменяется (что обычно происходит автоматически, если объект неизменен).

Кроме того, что, если я хочу, чтобы поиск в словаре и т. Д. Основывался на равенстве ссылок, а не на моих переопределенных Equals?

Ну, найдите словарную реализацию, которая работает так. Но стандартные словари библиотеки используют хэш-код и Equals, и изменить это невозможно.

В первую очередь я переопределяю Equals для простоты модульного тестирования моего кода сериализации, который, как я предполагаю, сериализацией и десериализацией (в моем случае, XML) убивает ссылочное равенство, поэтому я хочу убедиться, что он по крайней мере верен по значению равенства. Это плохая практика переопределять Equals в этом случае?

Нет, я бы нашел это вполне приемлемым. Однако вы не должны использовать такие объекты как ключи в словаре / хэш-таблице, поскольку они изменчивы. Смотри выше.

1 голос
/ 17 мая 2009

Я не знаю, как C # является относительной нубой к нему, но в Java, если вы переопределяете equals (), вам также нужно переопределить hashCode (), чтобы поддерживать контракт между ними (и наоборот) .. И у java тоже есть такой же улов 22; принуждая вас использовать неизменяемые поля ... Но это проблема только для классов, которые используются в качестве хеш-ключа, а в Java есть альтернативные реализации для всех коллекций на основе хеша ... что, возможно, не так быстро, но они эффективны позволяет вам использовать изменяемый объект в качестве ключа ... он просто (обычно) выглядит как "плохой дизайн".

И я испытываю желание указать, что эта фундаментальная проблема не подвластна времени ... Она существовала с тех пор, как Адам был мальчиком.

Я работал над кодом Фортрана, который старше меня (мне 36 лет), который ломается при смене имени пользователя (например, когда девушка выходит замуж или развелась ;-) ... Таким образом, инженерия, Было принято следующее решение: «метод» GetHashCode запоминает ранее вычисленный hashCode, пересчитывает hashCode (т. е. виртуальный маркер isDirty) и, если ключевые поля изменились, возвращает нуль. Это приводит к тому, что кеш удаляет «грязного» пользователя (путем вызова другого GetPreviousHashCode), а затем кеш возвращает ноль, в результате чего пользователь перечитывает данные из базы данных. Интересный и стоящий взлом; даже если я сам так говорю; -)

Я поменяю изменчивость (желательно только в угловых случаях) для доступа O (1) (желательно во всех случаях). Добро пожаловать в инжиниринг; земля осознанного компромисса.

Приветствия. Кит.

1 голос
/ 17 мая 2009

Основная тема здесь - как наилучшим образом идентифицировать объекты. Вы упоминаете сериализацию / десериализацию, которая важна, потому что ссылочная целостность теряется в этом процессе.

Краткий ответ: уникальна ли идентификация объектов по наименьшему набору неизменяемых полей, которые можно использовать для этого? Это поля, которые вы должны использовать при переопределении GetHashCode и Equals.

Для тестирования вполне разумно определить любые необходимые утверждения, обычно они определяются не для самого типа, а скорее как служебные методы в наборе тестов. Может быть, TestSuite.AssertEquals (MyClass, MyClass)?

Обратите внимание, что GetHashCode и Equals должны работать вместе. GetHashCode должен возвращать одинаковое значение для двух объектов, если они равны. Функция Equals должна возвращать true тогда и только тогда, когда два объекта имеют одинаковый хэш-код. (Обратите внимание, что два объекта могут быть не равны, но могут возвращать один и тот же хэш-код). Существует множество веб-страниц, посвященных этой теме, просто Google.

...