Что вернуть при переопределении Object.GetHashCode() в классах без неизменяемых полей?

Хорошо, прежде чем вы сойдете с ума из-за того, что в Интернете размещены сотни похожих по звучанию вопросов, я могу заверить вас, что я провел последние несколько часов, читая все и не нашел ответа. на мой вопрос.

Задний план:

По сути, одно из моих крупномасштабных приложений страдало от ситуации, когда некоторые Binding в свойстве ListBox.SelectedItem переставали работать или программа аварийно завершала работу после внесения изменений в текущий выбранный элемент. Сначала я задал 'Элемент с таким же ключом уже добавлен'. Исключение при выборе ListBoxItem из кода вопрос здесь, но ответов нет.

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

Теперь для тех из вас, кто не знает об этой проблеме, я обнаружил, что вы можете реализовать метод GetHashCode только с использованием неизменяемых полей/свойств. Используя отрывок из ответа Харви Квока на сообщение Overriding GetHashCode(), чтобы объяснить это:

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

Таким образом, настоящая проблема была вызвана тем, что я использовал свойства mutable в методах GetHashCode. Когда пользователи изменяли значения этих свойств в пользовательском интерфейсе, изменялись связанные значения хэш-кода объектов, после чего элементы больше не могли быть найдены в их коллекциях.

Вопрос:

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

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

Кроме того, статья Рекомендации и правила для GetHashCode в блоге Эрика Липперта есть раздел под названием Рекомендация: распределение хэш-кодов должно быть случайным, в котором подчеркиваются подводные камни использования алгоритма, приводящего к использованию недостаточного количества сегментов. Он предупреждает об алгоритмах, которые сокращают количество используемых сегментов и вызывают проблемы с производительностью, когда сегмент становится слишком большим. Конечно, возврат константы попадает в эту категорию.

У меня возникла идея добавить дополнительное поле Guid ко всем моим классам типов данных (только в C#, а не в базу данных) специально для использования в методе GetHashCode и только в нем. Итак, я полагаю, что в конце этого длинного вступления мой актуальный вопрос заключается в том, какая реализация лучше? Обобщить:

Резюме:

При переопределении Object.GetHashCode() в классах без неизменяемых полей лучше ли вернуть константу из метода GetHashCode или создать дополнительное поле readonly для каждого класса, которое будет использоваться исключительно в методе GetHashCode? Если мне нужно добавить новое поле, какого типа оно должно быть и не следует ли включать его в метод Equals?

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


person Sheridan    schedule 31.10.2013    source источник
comment
Если у вас есть экземпляр Effective C#, пункт номер 7 как раз об этом, если вы его еще не читали.   -  person JMK    schedule 31.10.2013
comment
Простой обходной путь, если использование ограничено одним местом, состоит в том, чтобы обернуть тип в другой класс, который предоставляет уникальное неизменное значение, вероятно, Guid, и использовать его для получения хэш-кода. Я бы лично старался не добавлять Guid к типу только для использования в словаре. Опять же, в качестве альтернативы вы можете использовать что-то вроде карты идентификаторов для ключа для объектов на основе отдельного неизменяемого идентификатора (опять же, вероятно, Guid, поэтому в результате получается тот же результат). Или, опять же, не меняйте элементы в словаре. Исключайте их, удаляйте, изменяйте, добавляйте заново.   -  person Adam Houldsworth    schedule 31.10.2013
comment
Не могли бы вы просто присвоить классу еще одно приватное поле и сделать его изменяемым (например, с помощью guid, как предлагает Адам) и использовать его для своего хеш-кода?   -  person David Arno    schedule 31.10.2013
comment
Спасибо, парни. @JMK, у меня случайно нет этой книги... не хочешь меня просветить? Адам, у меня есть одна проблема: я использую WPF, где все мои объекты привязаны к данным. Чтобы добавить код в каждую модель представления, который удалял бы выбранный элемент для редактирования, а затем повторно вставлял бы его, потребовалась бы вечность. Дэвид, я предложил это в своем вопросе... ну, во всяком случае, для свойства, но я имел в виду поле... Я обновлю вопрос.   -  person Sheridan    schedule 31.10.2013
comment
@Sheridan Если бы я попытался просветить вас, я бы запутал вас, потому что он начинается с Это единственный пункт в этой книге, посвященный одной функции, которую вам следует избегать писать, и хотя я читал ее несколько раз, я не полностью понимать это. Я подожду, пока появится кто-нибудь поумнее, а потом проголосую!   -  person JMK    schedule 31.10.2013
comment
Спасибо @JMK, я посмотрю, смогу ли я найти копию этой книги.... это та же самая книга, которую я только что нашел здесь?   -  person Sheridan    schedule 31.10.2013
comment
Я нашел это в книгах Google, так что вы можете прочитать это там. Я настоятельно рекомендую купить копию, это небольшая книга, но насыщенная деталями.   -  person JMK    schedule 31.10.2013
comment
А, я нашел вашу начальную цитату @JMK, так что я думаю, что это одна и та же книга. Еще раз спасибо.   -  person Sheridan    schedule 31.10.2013
comment
Довольно неясно, зачем вообще нужно было переопределять эти методы. Хорошей отправной точкой является полное удаление переопределений Equals и GetHashCode, реализации по умолчанию, унаследованные от Object, превосходны и гарантируют уникальность объекта. Вы никогда не получите от них ошибку двойного ключа.   -  person Hans Passant    schedule 31.10.2013
comment
@HansPassant, я также использую методы Equals для уведомления об изменениях в приложении. Под уведомлением об изменении я имею в виду, что у меня есть свойство HasChanges, которое использует его следующим образом: return originalState != null && !this.Equals(originalState);. Если бы мне пришлось удалить реализации Equals и GetHashCode, разве мне не пришлось бы реализовывать IEqualityComparer<T> для каждого класса типов данных? У меня около 100 или больше, так что, может быть, я мог бы сделать это в моем следующем проекте.   -  person Sheridan    schedule 31.10.2013
comment
Ну, его не обязательно называть Equals(), не так ли? Вы можете называть это как угодно, Equals() нужно переопределять только тогда, когда это имеет значение для кода .NET Framework. Здесь это имеет значение, WPF не очень заботится об изменении возвращаемого значения Equals() во время привязки элемента.   -  person Hans Passant    schedule 31.10.2013
comment
@HansPassant, как удалить переопределения Equals и GetHashCode? На странице IEquatable<T>Interface в MSDN написано Это должно быть реализован для любого объекта, который может храниться в универсальной коллекции, а затем в IEquatable<T>.Equals Method говорится: Если вы реализуете Equals, вам также следует переопределить реализации базового класса Object.Equals(Object) и GetHashCode, чтобы их поведение соответствовало поведению IEquatable‹T. ›.   -  person Sheridan    schedule 02.11.2013


Ответы (5)


Вернитесь к основам. Вы читали мою статью; прочитайте это снова. Два железных правила, которые имеют отношение к вашей ситуации:

  • если x равно y, то хеш-код x должен быть равен хэш-коду y. Эквивалентно: если хэш-код x не равен хэш-коду y, тогда x и y должны быть неравны.
  • хэш-код x должен оставаться стабильным, пока x находится в хеш-таблице.

Это требования для правильности. Если вы не можете гарантировать эти две простые вещи, ваша программа не будет правильной.

Вы предлагаете два решения.

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

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

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

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

person Eric Lippert    schedule 01.11.2013
comment
+1 Спасибо, что нашли время дать понятный и понятный ответ. Однако я хотел бы обратить внимание на некоторые вещи, о которых вы сказали... Я не использую Dictionary или HashTable где-либо в своем коде. Я использую WPF и могу только предположить, что рассматриваемые HashTable или Dictionary используются внутри Framework. Глядя на StackTrace из связанного предыдущего вопроса, я вижу вызов System.Windows.DependencyObject.OnPropertyChanged, а затем System.Windows.Controls.ListBoxItem.OnSelected. Так что, как видите, я не могу это контролировать. - person Sheridan; 01.11.2013

Я бы либо создал дополнительное поле readonly, либо выкинул бы NotSupportedException. На мой взгляд, другой вариант бессмысленен. Давайте посмотрим, почему.

Отдельные (фиксированные) хэш-коды

Предоставление различных хэш-кодов легко, например:

class Sample
{
    private static int counter;
    private readonly int hashCode;

    public Sample() { this.hashCode = counter++; }

    public override int GetHashCode()
    {
        return this.hashCode;
    }

    public override bool Equals(object other)
    {
        return object.ReferenceEquals(this, other);
    }
}

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

Проблема с этим подходом заключается в том, что экземпляры никогда не будут сравниваться как равные. Однако это совершенно нормально, если вы хотите использовать только экземпляры Sample в качестве индексов в коллекции какого-либо другого типа.

Постоянные хэш-коды

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

Поиск экземпляра внутри контейнера всегда сводится к эквиваленту линейного поиска. Таким образом, возвращая константу, вы позволяете пользователю создать контейнер с ключом для вашего класса, но этот контейнер будет демонстрировать характеристики производительности LinkedList<T>. Это может быть очевидно для тех, кто знаком с вашим классом, но лично я считаю, что это позволяет людям стрелять себе в ногу. Если вы заранее знаете, что Dictionary не будет вести себя так, как можно было бы ожидать, то зачем позволять пользователю создавать его? На мой взгляд, лучше выкинуть NotSupportedException.

Но бросать - это то, чего делать нельзя!

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

Однако это еще не все. В своем сообщении в блоге тему, Эрик Липперт говорит, что если бросить изнутри GetHashCode, то

ваш объект не может быть результатом многих запросов LINQ-to-objects, которые используют внутренние хеш-таблицы из соображений производительности.

Потеря LINQ — это, конечно, облом, но, к счастью, на этом дорога не заканчивается. Многие (все?) методы LINQ, использующие хеш-таблицы, имеют перегрузки, которые принимают IEqualityComparer<T> для использования при хешировании. Таким образом, вы можете использовать LINQ, но это будет менее удобно.

В конце концов, вам придется взвешивать варианты самостоятельно. Мое мнение таково, что лучше работать со стратегией белого списка (предоставлять IEqualityComparer<T> всякий раз, когда это необходимо), если это технически возможно, потому что это делает код явным: если кто-то пытается использовать класс наивно, он получает исключение, которое полезно сообщает ему, что происходит, и компаратор равенства виден в коде, где бы он ни использовался, что сразу же делает неординарное поведение класса очевидным.

person Jon    schedule 31.10.2013
comment
Ну вот! +1 Я хотел попробовать ответить сам, но передумал! - person JMK; 31.10.2013
comment
+1 Прикомандирован! Спасибо, что нашли время написать такой полный ответ @Jon. Если бы я пошел с идеей «добавить поле», было бы нормально возвращать (эквивалентное значение) Guid.NewGuid().GetHashCode()? Не следует ли мне использовать это поле и в методе Equals? - person Sheridan; 31.10.2013
comment
@Sheridan: Это было бы хорошо, но, возможно, излишне. Технически вы могли бы использовать это и для сравнения внутри Equals, но, поскольку вы знаете, что он предназначен для обеспечения семантики равенства ссылок, вы также можете сделать это напрямую. - person Jon; 31.10.2013
comment
Привет, @Jon, прочитав больше об интерфейсе IEqualityComparer<T>, я немного запутался ... не будет ли реализация этого интерфейса или расширение класса EqualityComparer<T>, как это рекомендуется в MSDN, привести к той же проблеме, что и переопределение свойства Equals when значения меняются для объектов в коллекции? - person Sheridan; 02.11.2013
comment
@Sheridan: Было бы так, нет никакой магии, которая позволила бы съесть торт и съесть его. Но процесс наблюдения за тем, что класс не работает как есть, создание собственного компаратора равенства и его использование должно быть более чем достаточным указанием на то, что да, я знаю, что возвращение константы будет плохо для производительности, но сделайте это уже. Идея состоит в том, чтобы заставить пользователя вашего класса явно подписаться, а не позволять ему блаженно блуждать в ловушке. - person Jon; 02.11.2013
comment
Использование ReferenceEquals и никогда не равного хэш-кода не лучше, чем то, что object уже делает для вас, поэтому в этом случае вы могли бы вообще не реализовывать их. Вопрос имеет смысл только в тех контекстах, где вы не хотите, чтобы Equals было ссылочным равенством, и в этом случае хэш-коды не могут быть полностью уникальными, поскольку они должны быть равны, если Equals возвращает true. - person Miral; 05.04.2019

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

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

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

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

Или, возможно, мне вообще не следует переопределять Equals.

person Miral    schedule 05.04.2019
comment
Но мы хотим, чтобы у пользователей не возникало проблем, независимо от их вины. - person Sheridan; 08.04.2019
comment
У пользователей возникают проблемы только в том случае, если они не соблюдают правила. И я не вижу другой приемлемой альтернативы, кроме как сделать тип неизменяемым, что также является правильным выбором, но имеет другие последствия, которые в некоторых случаях могут быть нежелательными. - person Miral; 09.04.2019

Если классы действительно не содержат ничего постоянного, по которому можно вычислить хеш-значение, я бы использовал что-то более простое, чем GUID. Просто используйте случайное число, сохраненное в классе (или в классе-оболочке).

person Dweeberly    schedule 31.10.2013
comment
Спасибо @Dweeberly. могу я спросить, что заставляет вас думать, что это будет лучше, чем Guid? - person Sheridan; 31.10.2013
comment
Он меньше и проще в создании. В большинстве случаев использование хэша модифицирует значение с помощью простого числа, чтобы получить адрес корзины. GetHashCode возвращает 32-битное целое число, поэтому все, что больше этого, является излишним. - person Dweeberly; 31.10.2013

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

private int? _hashCode;

public override int GetHashCode() {
   if (!_hashCode.HasValue)
      _hashCode = Property1.GetHashCode() ^ Property2.GetHashCode() etc... based on whatever you use in your equals method
   return _hashCode.Value;
}

Однако, если у вас есть, скажем, объект a и объект b, где a.Equals(b) == true, и вы сохраняете запись в своем словаре, используя a в качестве ключа (dictionary[a] = value).
Если а не изменится, то словарь[b] вернет значение, однако, если изменить а после сохранения записи в словаре, то словарь[b], скорее всего, не удастся. Единственный обходной путь — перефразировать словарь при изменении любого из ключей.

person Martin Ernst    schedule 31.10.2013
comment
Спасибо @MartinErnst. Проблема, связанная с этим методом, заключается в том, что все свойства класса используются в методе Equals, и они все изменяемы. Сначала я подумал, что могу использовать свойства 'Id' и DateCreated в GetHashCode, потому что они никогда не меняются, но потом я понял, что даже эти свойства изменяются при добавлении нового элемента. - person Sheridan; 31.10.2013
comment
Кажется, что ключи, которые вы используете, являются сущностями - проще всего было бы использовать идентификатор в качестве ключа в вашем словаре и не добавлять новые записи, пока им не будет назначен идентификатор, если это возможно. В качестве альтернативы вы можете использовать описанный выше метод и сгенерировать хэш-код только на основе свойства id, если сущность не является новой сущностью, и если это новая сущность (скажем, идентификатор равен 0), то используйте base.GetHashCode(). Если вы используете новые объекты в качестве ключей в словаре, который переживает контекст/сеанс данных, вам нужно будет перефразировать словарь, когда объекту будет присвоен новый идентификатор. - person Martin Ernst; 04.11.2013
comment
Спасибо, что ответили мне на этот вопрос... не могли бы вы объяснить, что вы подразумеваете под перефразировать словарь? Кроме того, возможно ли это для коллекции, данные которой привязаны к WPF ListBox? - person Sheridan; 04.11.2013
comment
В основном удалите элемент из словаря и добавьте его снова. - person Martin Ernst; 04.11.2013