Почему важно переопределить GetHashCode, когда метод Equals переопределен?

Учитывая следующий class

public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Which is preferred? return base.GetHashCode(); //return this.FooId.GetHashCode(); } } 

Я переопределил метод Equals потому что Foo представляет строку для таблицы Foo . Каков предпочтительный метод переопределения GetHashCode ?

Почему важно переопределить GetHashCode ?

Да, важно, чтобы ваш элемент использовался как ключ в словаре, или HashSet и т. Д. – поскольку он используется (при отсутствии пользовательского IEqualityComparer ) для группировки элементов в ведра. Если hash-код для двух элементов не совпадает, они никогда не могут считаться равными (равные просто не будут вызываться).

Метод GetHashCode() должен отражать логику Equals ; Правила:

  • если две вещи равны ( Equals(...) == true ), то они должны возвращать то же значение для GetHashCode()
  • если GetHashCode() равен, для них не обязательно быть одинаковым; это столкновение, и Equals будут вызывать, чтобы убедиться, что это реальное равенство или нет.

В этом случае он выглядит как « return FooId; » – это подходящая реализация GetHashCode() . Если вы тестируете несколько свойств, обычно их объединяют с использованием кода, как показано ниже, для уменьшения диагональных коллизий (т. Е. Для new Foo(3,5) имеет новый хеш-код для new Foo(5,3) ):

 int hash = 13; hash = (hash * 7) + field1.GetHashCode(); hash = (hash * 7) + field2.GetHashCode(); ... return hash; 

О – для удобства вы можете также рассмотреть возможность предоставления == и != Операторов при переопределении Equals и GetHashCode .


Демонстрация того, что происходит, когда вы ошибаетесь, здесь .

На самом деле очень сложно реализовать GetHashCode() правильно, потому что, помимо уже упомянутых правил Marc, hash-код не должен меняться в течение всего жизненного цикла объекта. Поэтому поля, которые используются для вычисления hash-кода, должны быть неизменными.

Наконец, я нашел решение этой проблемы, когда я работал с NHibernate. Мой подход заключается в вычислении хеш-кода из идентификатора объекта. Идентификатор может быть установлен только с помощью конструктора, поэтому, если вы хотите изменить ID, что очень маловероятно, вам нужно создать новый объект с новым идентификатором и, следовательно, новый hash-код. Этот подход лучше всего работает с GUID, потому что вы можете предоставить конструктор без параметров, который случайным образом генерирует идентификатор.

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

Это пример того, как ReSharper пишет функцию GetHashCode () для вас:

 public override int GetHashCode() { unchecked { var result = 0; result = (result * 397) ^ m_someVar1; result = (result * 397) ^ m_someVar2; result = (result * 397) ^ m_someVar3; result = (result * 397) ^ m_someVar4; return result; } } 

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

Не забывайте проверять параметр obj на значение null при переопределении Equals() . А также сравнить тип.

 public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } 

Причина этого: Equals должны возвращать false при сравнении с null . См. Также http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

Как насчет:

 public override int GetHashCode() { return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode(); } 

Предполагая, что производительность не проблема 🙂

Это связано с тем, что фреймворк требует, чтобы два объекта, которые одинаковы, должны иметь один и тот же hash-код. Если вы переопределите метод equals, чтобы выполнить специальное сравнение двух объектов, и оба объекта считаются одинаковыми с помощью метода, то hash-код двух объектов также должен быть одинаковым. (Словари и Hashtables полагаются на этот принцип).

Просто добавьте ответы выше:

Если вы не переопределяете Equals, то по умолчанию используется сравнение ссылок объектов. То же самое относится к hashcode – приведение по умолчанию обычно основано на адресе памяти ссылки. Поскольку вы переопределили Equals, это означает, что правильное поведение заключается в сравнении того, что вы внедрили в Equals, а не в ссылках, поэтому вы должны сделать то же самое для hashcode.

Клиенты вашего classа ожидают, что hash-код будет иметь аналогичную логику для метода equals, например, методы linq, которые используют IEqualityComparer, сначала сравнивают hash-коды и только если они равны, они будут сравнивать метод Equals (), который может быть более дорогим для запуска, если бы мы не реализовали hashcode, у равного объекта, вероятно, будут разные hash-коды (потому что они имеют другой адрес памяти) и будут ошибочно определены как не равные (Equals () даже не попадет).

Кроме того, кроме проблемы, что вы не сможете найти свой объект, если вы использовали его в словаре (потому что он был вставлен одним hash-кодом, и когда вы его ищете, hash-код по умолчанию, вероятно, будет другим, и снова Equals () даже не будет вызван, как объясняет Марк Гравелл в своем ответе, вы также вводите нарушение концепции словаря или хешета, которая не должна позволять идентичные ключи – вы уже заявили, что эти объекты по существу одинаковы, когда вы перегружаете Equals, чтобы вы не Не хотите, чтобы оба они были разными ключами в структуре данных, которые предполагают наличие уникального ключа. Но поскольку у них есть другой hash-код, «тот же» ключ будет вставлен как другой.

У нас есть две проблемы, с которыми можно справиться.

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

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

Поэтому по умолчанию я это делаю.

 public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Some comment to explain if there is a real problem with providing GetHashCode() // or if I just don't see a need for it for the given class throw new Exception("Sorry I don't know what GetHashCode should do for this class"); } } 

Хэш-код используется для hash-коллекций, таких как Dictionary, Hashtable, HashSet и т. Д. objective этого кода – очень быстро предварительно сортировать конкретный объект, помещая его в определенную группу (ведро). Эта предварительная сортировка очень помогает в поиске этого объекта, когда вам нужно вернуть его из коллекции хешей, потому что код должен искать ваш объект только в одном ведро, а не во всех его объектах. Лучшее распределение hash-кодов (лучшая уникальность) – более быстрое извлечение. В идеальной ситуации, когда каждый объект имеет уникальный хеш-код, поиск его является операцией O (1). В большинстве случаев он приближается к O (1).

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

  public override int GetHashCode() { return base.GetHashCode(); } 

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

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

 class A { public int Value; public override int GetHashCode() { return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time } } 

С другой стороны, если значение не может быть изменено, это нормально использовать:

 class A { public readonly int Value; public override int GetHashCode() { return Value.GetHashCode(); //OK Value is read-only and can't be changed during the instance's life time } } 

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

EDITED: Это неверно, оригинальный метод GetHashCode () не может гарантировать равенство двух значений. Хотя объекты, которые равны, возвращают один и тот же hash-код.

Ниже, используя reflection, кажется мне лучшим вариантом с учетом публичных свойств, так как с этим вам не нужно беспокоиться о добавлении / удалении свойств (хотя это не так распространенный сценарий). Это, как мне показалось, лучше работает. (По сравнению с секундомером).

  public int getHashCode() { PropertyInfo[] theProperties = this.GetType().GetProperties(); int hash = 31; foreach (PropertyInfo info in theProperties) { if (info != null) { var value = info.GetValue(this,null); if(value != null) unchecked { hash = 29 * hash ^ value.GetHashCode(); } } } return hash; } 
  • Дополнительные параметры C # для переопределенных методов
  • Почему я не могу получить доступ к C # защищенным членам, кроме этого?
  • Как аннотация метода Java работает в сочетании с переопределением метода?
  • «Тени» против «Переопределения» в VB.NET
  • Давайте будем гением компьютера.