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

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

Справочная информация:

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

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

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

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

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

Вопрос:

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

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

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

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

Резюме:

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

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

Ответ 1

Вернитесь к основам. Вы читаете мою статью; прочитайте его снова. Эти две жесткие правила, которые имеют отношение к вашей ситуации:

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

Ответ 4

Простым подходом является сохранение hashCode в частном члене и генерация его при первом использовании. Если ваша сущность не меняется часто, и вы не собираетесь использовать два разных объекта, равных (где ваш метод 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, и вы сохраняете запись в словаре, используя ключ as (словарь [a] = значение).
Если a не изменится, то словарь [b] вернет значение, однако, если вы измените после сохранения записи в словаре, то словарь [b] скорее всего потерпит неудачу. Единственное обходное решение для этого - перефразировать словарь при изменении любого из ключей.

Ответ 5

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

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

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

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

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