Переопределение GetHashCode для изменяемых объектов?

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

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

РЕДАКТИРОВАТЬ:

Кажется, мои намерения были неясны, я имею в виду, что в 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 для ссылочных типов, поскольку, как я видел, большинство ссылочных типов обычно не имеют неизменяемых полей, если они не хранятся в реляционной базе данных, чтобы идентифицировать их с их первичными ключами.


person Davy8    schedule 17.05.2009    source источник
comment
И если в вашем классе нет неизменяемых полей, и вы не используете ссылочное равенство, тогда хэш-код должен быть ... ах ... константой! Разрушит ли это O (1) природу поиска в словаре? - несомненно. Это хоть в словарях будет правильно? - Да.   -  person user420667    schedule 05.05.2016


Ответы (5)


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

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

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

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

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

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

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

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

person JaredPar    schedule 17.05.2009
comment
Если вы ожидаете, что ваши объекты будут вести себя с равенством значений, вам следует переопределить == и! =, Чтобы обеспечить равенство значений для всех сравнений. Пользователи по-прежнему могут использовать Object.ReferenceEquals, если им действительно нужно ссылочное равенство. Но в том-то и дело, я не ожидаю, что они будут вести себя с равенством значений, я ожидаю, что они будут вести себя со ссылочным равенством, за исключением случаев, когда я явно хочу, чтобы они этого не делали, используя object.Equals, во всех других обстоятельствах я ожидаю ссылочного равенства, как и большинство других классы. - person Davy8; 17.05.2009
comment
Отредактировал вопрос, чтобы уточнить реальный вопрос. - person Davy8; 17.05.2009
comment
К сожалению, это проблема, которая обычно возникает только в крайних случаях. Таким образом, разработчики склонны избегать плохого поведения. Здесь упоминается, что людям сходит с рук плохое поведение? Как же тогда делать это правильно? Есть несколько предложений по основанию хэш-кода на неизменяемых полях, но что, если неизменных полей нет? - person Davy8; 17.05.2009
comment
@ Davy8, лучший способ - использовать неизменяемые поля. Таким образом, он работает в 100% случаев. Если неизменяемых полей нет, возможно, вы захотите немного переосмыслить свой дизайн. Либо сделайте подмножество полей неизменяемым, либо не реализуйте равенство через .Equals. В противном случае у вас будет только в основном рабочее решение. - person JaredPar; 17.05.2009
comment
Итак, чтобы подвести итог, лучшая практика - переопределять только Equals (и GetHashCode) только на основе неизменяемых полей, а изменяемое равенство желательно тогда для реализации нового метода для сравнения такого равенства? - person Davy8; 17.05.2009
comment
@ Davy8 да. Это единственный способ надежно обеспечить равенство. - person JaredPar; 17.05.2009

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

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

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

person Guffa    schedule 17.05.2009
comment
Может быть, тогда я просто неправильно использовал изменяемые объекты в качестве ключей. Я всегда использовал их и ожидал, что они будут использовать ссылочное равенство, например var a = new object (); var b = новый объект (); dict [a] = привет; dict [b] = мир; - person Davy8; 17.05.2009
comment
Когда объект используется в качестве ключа в хэшированной коллекции, используются методы GetHashCode и Equals. Если они не переопределены, реализация по умолчанию (Object) должна использовать ссылочное равенство. - person Guffa; 17.05.2009
comment
Верно, и я предполагаю, что было бы наименее удивительным, если бы они всегда основывались на ==, что не рекомендуется переопределять, а не на Equals, которое более приемлемо для перезаписи. Все это, конечно же, мнение. - person Davy8; 17.05.2009
comment
Что ж, основание на == приведет к довольно странным результатам, если вы используете примитивные объекты (такие как строки), потому что a + b! = Ab, что скорее испортит поиск. - person sleske; 17.05.2009
comment
Одна фундаментальная слабость в Java, которая проявляется в .NET, заключается в том, что нет различия между переменной, которая используется для инкапсуляции идентичности изменяемого объекта типа Foo, и переменной, которая используется для инкапсулируют состояние экземпляра Foo, который никогда не будет представлен коду, который мог бы его изменить. Семантика Equals и GethashCode для последнего должна отличаться от первой, но для Foo нет механизма, позволяющего предоставлять разные наборы методов для этих двух целей. - person supercat; 16.06.2013
comment
@supercat: Я не понимаю, что вы имеете в виду. Не существует типа, который мог бы быть типом значения или ссылочным типом, каждый тип всегда является одним или другим. - person Guffa; 17.06.2013
comment
@Guffa: Предположим, что единственное поле в классе Foo - это int[] Arr. Существует два экземпляра Foo, каждый со ссылкой на другой массив из 500 элементов, но оба массива содержат одинаковые 500 значений. Следует ли считать экземпляры Foo эквивалентными или нет? Если Arr используется для идентификации массива, который используется во внешнем коде и значения которого Foo должны увеличиваться каждый раз, когда вызывается его IncrementValues метод, экземпляры Foo явно не эквивалентны. Если он используется для хранения неизменного набора из 500 значений, эти два экземпляра явно эквивалентны. - person supercat; 17.06.2013
comment
@Guffa: Если два массива никогда не будут доступны для чего-либо за пределами Foo, и если Foo может изменять их, но только по запросу, то два экземпляра Foo, которые хранятся кодом, который никогда не попросит его изменить массивы, могут считаться эквивалентными , но экземпляры, принадлежащие коду, который может их видоизменять, следует рассматривать как отдельные. Если бы я писал спецификации для методов Equivalent и EqualState, я бы указал, что x.Equivalent(y) должен возвращать истину, если у объекта есть основания полагать, что замена любых произвольных ссылок на x ссылками на y ... - person supercat; 17.06.2013
comment
... не повлияет на поведение каких-либо членов типа; x.EqualState(y) должен возвращать истину, если одновременная замена всех ссылок на x ссылками на y и наоборот не повлияет на поведение каких-либо членов типа. Обычно ожидается, что Object.Equivalent() будет проверять ссылочную идентичность для изменяемых типов или соответствовать Object.EqualState() для неизменяемых. - person supercat; 17.06.2013
comment
@supercat: В вашем примере ни один из случаев не всегда явно не эквивалентен или всегда явно эквивалентен. Даже если у вас есть два экземпляра, которые ссылаются на один и тот же массив, вы не можете предположить, что они всегда должны считаться эквивалентными во всех возможных ситуациях. Использование двух разных методов для сравнения не помогает. Вы можете указать дюжину различных методов для сравнения объектов в разных аспектах, но они все равно не охватят все возможные ситуации. - person Guffa; 17.06.2013
comment
@Guffa: он может не охватывать абсолютно все ситуации, но он поможет с тем фактом, что .NET не имеет концепции неизменяемого экземпляра изменяемого типа. Код, который создает Thingie и никогда не предоставляет его никакому коду, который мог бы изменить, он может знать, что, хотя Thingie является изменяемым, этот конкретный экземпляр никогда не может измениться [т.е. нет последовательности выполнения, которая заставила бы это сделать]. Эквивалентность должна означать разные вещи для изменяемых и неизменяемых экземпляров, но .NET не предоставляет никаких средств для проведения различия. - person supercat; 17.06.2013
comment
@supercat: Проблема с введением подобных концепций равенства заключается в том, что нет способа обеспечить соблюдение ограничений, на которые они полагаются, поэтому это может открыть несколько проблем, которые могут быть столь же серьезными, как и проблемы, которые он пытается решить. Изменяемый объект нельзя сделать неизменяемым, кроме как инкапсулировать, и нет никакого способа гарантировать, что сравнение неизменяемых экземпляров не может использоваться для экземпляров, которые могут быть изменены. - person Guffa; 17.06.2013
comment
@Guffa: Нет никакого способа, которым система действительно может обеспечить соблюдение большей части чего-либо, связанного с равенством, но достаточное количество классов следует правилам, чтобы сделать классы, которые полагаются на правила (например, Dictionary), пригодными для использования. Как вы заметили, создание неизменяемого экземпляра изменяемого типа требует инкапсуляции, но инкапсуляция - очень распространенный способ создания фактически неизменяемых экземпляров изменяемых типов. Вопрос в том, должен ли инкапсулирующий класс сам выполнять хеширование и сравнение, или он должен иметь какие-то средства для того, чтобы просить инкапсулированный объект сделать это. Я бы предпочел позднее. - person supercat; 17.06.2013

Вау, на самом деле это несколько вопросов в одном :-). Так по порядку:

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

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

Чтобы процитировать документацию интерфейса Java Map:

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

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

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

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

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

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

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

person sleske    schedule 17.05.2009
comment
Итак, вы говорите, что вопрос о том, что хэш-коды не меняются, не так важен, потому что вы не должны использовать изменяемые объекты, в первую очередь ключи словаря? - person Davy8; 17.05.2009

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

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

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

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

Ваше здоровье. Кит.

person corlettk    schedule 17.05.2009
comment
Что вы подразумеваете под классом, который позволяет вам использовать изменяемый объект в качестве ключа? Java действительно позволяет это, просто он не работает (т.е. не может) работать, если ключ действительно изменяется, потому что просто не ясно, что он должен делать. - person sleske; 18.05.2009

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

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

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

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

person Joe    schedule 17.05.2009
comment
А что, если у объекта нет неизменяемых полей? - person Davy8; 17.05.2009
comment
Я погуглил и искал SO и нашел много ответов, которые, похоже, напрямую не решают мою проблему. Смотрите мою правку для получения дополнительной информации. - person Davy8; 17.05.2009
comment
Если у вас нет неизменяемых полей, вы всегда можете создать синтетическое, это может быть либо последовательность из БД, либо глобально уникальный идентификатор (GUID), если у вас нет БД. Переопределите hash / equals, чтобы использовать это значение guid, и убедитесь, что вы всегда включаете синтетический идентификатор при сериализации / десериализации. Кроме того, когда вы создаете новые объекты этого типа, вам может потребоваться выполнить полное сканирование существующих объектов этого типа, вызывая Equals для каждого, и посмотреть, есть ли у вас уже тот же объект с идентификатором и, если да, новый объект должен иметь такой же Идентификатор сопоставленного объекта ... - person Joe; 19.05.2009