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


1445

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

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null) 
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

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

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


36
Важно реализовать как equals, так и gethashcode из-за коллизий, в частности, при использовании словарей. если два объекта возвращают один и тот же хеш-код, они вставляются в словарь с цепочкой. При доступе к элементу равно метод используется.
DarthVader

Ответы:


1320

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

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

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

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

unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
    int hash = 13;
    hash = (hash * 7) + field1.GetHashCode();
    hash = (hash * 7) + field2.GetHashCode();
    ...
    return hash;
}

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


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


49
Могу я спросить, а умножаете ли вы эти факторы?
Леандро Лопес

22
На самом деле, я мог бы потерять одного из них; смысл в том, чтобы попытаться свести к минимуму количество столкновений - чтобы объект {1,0,0} имел хеш-код, отличающийся от {0,1,0} и {0,0,1} (если вы понимаете, о чем я ),
Марк Гравелл

13
Я подправил числа, чтобы они стали понятнее (и добавил семя). В некотором коде используются разные числа - например, компилятор C # (для анонимных типов) использует начальное число 0x51ed270b и коэффициент -1521134295.
Марк Гравелл

76
@ Леандро Лопес: Обычно факторы выбираются как простые числа, потому что это делает число столкновений меньше.
Андрей Ринея

29
«О - для удобства вы могли бы также рассмотреть возможность предоставления операторов == и! = При переопределении Equals и GethashCode.»: Microsoft не рекомендует реализовывать оператор == для объектов, которые не являются неизменяемыми - msdn.microsoft.com/en-us/library/ ms173147.aspx - «Не рекомендуется переопределять оператор == в неизменяемых типах».
антидух

137

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

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


20
@vanja. Я полагаю, что это связано с тем, что: если вы добавляете объект в словарь, а затем изменяете идентификатор объекта, при извлечении позже вы будете использовать другой хеш для его извлечения, так что вы никогда не получите его из словаря.
Ноябрь

74
Документация Microsoft о функции GetHashCode () не утверждает и не подразумевает, что хэш объекта должен оставаться согласованным в течение всего срока его службы. На самом деле, он конкретно объясняет один допустимый случай, в котором он может и не быть : «Метод GetHashCode для объекта должен последовательно возвращать один и тот же хеш-код, если нет изменения в состоянии объекта, которое определяет возвращаемое значение метода Equals объекта «.
PeterAllenWebb

37
«хеш-код не должен изменяться в течение жизни объекта» - это неправда.
Апокалипсис

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

11
@ScottChamberlain Я думаю, что вы забыли НЕ в своем комментарии, оно должно быть: «хеш-код (или эквивалентность равно) не должны меняться в течение периода, когда объект используется в качестве ключа для коллекции». Правильно?
Стэн Прокоп,

57

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

Это пример того, как 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;
    }
}

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


7
Разве это не всегда возвращает ноль? Вероятно, следует инициализировать результат 1! Также нужно еще несколько точек с запятой.
Сэм Маккрилл

16
Вы знаете, что делает оператор XOR (^)?
Стивен Дрю

1
Как я уже сказал, это то, что R # пишет для вас (по крайней мере, это было сделано в 2008 году), когда его об этом попросили. Очевидно, этот фрагмент предназначен для некоторой подстройки программистом. Что касается отсутствующих точек с запятой ... да, похоже, я их пропустил, когда скопировал код из области выбора в Visual Studio. Я также думал, что люди поймут это оба.
Ловушка

3
@SamMackrill Я добавил в пропущенные точки с запятой.
Мэтью Мердок

5
@SamMackrill Нет, это не всегда будет возвращать 0. 0 ^ a = a, так 0 ^ m_someVar1 = m_someVar1. Он мог бы также установить начальное значение resultв m_someVar1.
Милли Смит

41

Пожалуйста, не забудьте проверить параметр obj nullпри переопределении Equals(). А также сравните тип.

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

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


6
Эта проверка типа не будет выполнена в ситуации, когда подкласс ссылается на метод Equals суперкласса как часть собственного сравнения (т. Е. Base.Equals (obj)) - следует использовать вместо него
sweetfa

@sweetfa: Это зависит от того, как реализован метод Equals подкласса. Он также может вызвать base.Equals ((BaseType) obj)), который будет работать нормально.
Хаха

2
Нет, не будет: msdn.microsoft.com/en-us/library/system.object.gettype.aspx . Кроме того, реализация метода не должна завершаться сбоем или завершаться успешно в зависимости от способа его вызова. Если тип времени выполнения объекта является подклассом некоторого базового класса, то Equals () базового класса должен возвращать true, если он objдействительно равен thisнезависимо от того, как вызывался Equals () базового класса.
Юпитер

2
Перемещение fooItemвверх и проверка его на нулевое значение будет работать лучше в случае нулевого или неправильного типа.
IllidanS4 хочет вернуть Монику

1
@ 40Alpha Ну да, тогда obj as Fooбудет недействительным.
IllidanS4 хочет вернуть Монику

35

Как насчет:

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

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


1
эм, но вы возвращаете строку для метода, основанного на int; _0
jim tollan

32
Нет, он вызывает GetHashCode () из объекта String, который возвращает int.
Ричард Клэйтон

3
Я не ожидаю, что это будет так быстро, как хотелось бы, не только для бокса, используемого для типов значений, но и для производительности string.Format. Еще один вызывающий, который я видел, это new { prop1, prop2, prop3 }.GetHashCode(). Не могу прокомментировать, хотя какой из них будет медленнее между этими двумя. Не злоупотребляйте инструментами.
Nawfal

16
Это вернет истину для { prop1="_X", prop2="Y", prop3="Z" }и { prop1="", prop2="X_Y", prop3="Z_" }. Вы, вероятно, не хотите этого.
voetsjoeba

2
Да, вы всегда можете заменить символ подчеркивания чем-то необычным (например, •, ▲, ►, ◄, ☺, ☻) и надеяться, что ваши пользователи не будут использовать эти символы ... :)
Ludmil Tinkov

13

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

  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;

        if (fooItem == null)
        {
           return false;
        }

        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");
    }
}

5
Создание исключения из GetHashCode является нарушением контракта объекта. Нетрудно определить GetHashCodeфункцию, чтобы любые два равных объекта возвращали один и тот же хэш-код; return 24601;и return 8675309;оба будут действительными реализациями GetHashCode. Производительность Dictionaryбудет приличной только тогда, когда количество предметов невелико, и будет очень плохой, если количество предметов увеличится, но в любом случае она будет работать правильно.
Суперкат

2
@supercat, Невозможно разумно реализовать GetHashCode, если поля идентификации в объекте могут измениться, поскольку хеш-код никогда не должен изменяться. Выполнение того, что вы говорите, может привести к тому, что кому-то придется потратить много дней на то, чтобы отследить проблему с производительностью, а затем на недели, чтобы перепроектировать большую систему, чтобы исключить использование словарей.
Ян Рингроз

2
Раньше я делал что-то подобное для всех определенных мной классов, для которых требовался Equals (), и где я был полностью уверен, что никогда не буду использовать этот объект в качестве ключа в коллекции. Затем однажды программа, в которой я использовал такой объект в качестве входных данных для элемента управления DevExpress XtraGrid, потерпела крах. Оказывается, XtraGrid за моей спиной создавал HashTable или что-то на основе моих объектов. Я немного поспорил со службой поддержки DevExpress по этому поводу. Я сказал, что было неумно, что они основывали функциональность и надежность своих компонентов на неизвестной клиентской реализации неясного метода.
RenniePet

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

4
@RenniePet, лучше быть влюбленным из-за исключения, а затем из-за неправильной реализации очень трудно найти ошибку.
Ян Рингроз

12

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


11

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

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

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

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


8

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


7

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

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

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

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


   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
      }
   }

3
Downvoted. Это совершенно неправильно. Даже Microsoft заявляет в MSDN ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ), что значение GetHashCode ДОЛЖНО изменяться при изменении состояния объекта таким образом, что это может повлиять на возвращаемое значение вызова в Equals (), и даже в своих примерах он также показывает реализации GetHashCode, которые полностью зависят от общедоступных значений.
Себастьян пиар Гингтер

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

2
Себастьян, Кроме того, я не вижу в ссылке утверждение ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ), что GetHashCode () должен измениться. Напротив, он НЕ должен изменяться, пока Equals возвращает одно и то же значение для одного и того же аргумента: «Метод GetHashCode для объекта должен последовательно возвращать один и тот же хеш-код, если нет изменения состояния объекта, определяющего возвращаемое значение. метода Equals объекта. "Этот оператор не подразумевает обратное, что он должен измениться, если изменяется возвращаемое значение для Equals.
ILoveFortran

2
@Joao, вы путаете сторону клиента / потребителя в контракте с производителем / исполнителем. Я говорю об ответственности исполнителя, который переопределяет GetHashCode (). Вы говорите о потребителе, о том, кто использует ценность.
ILoveFortran

1
Полное недоразумение ... :) Правда в том, что хеш-код должен меняться при изменении состояния объекта, если только это состояние не имеет отношения к идентичности объекта. Кроме того, вы никогда не должны использовать объект MUTABLE в качестве ключа в ваших коллекциях. Используйте объекты только для чтения для этой цели. GetHashCode, Equals ... и некоторые другие методы, имена которых я не помню в данный момент, НИКОГДА не должны выбрасывать.
дорогая

0

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

Например, предположим, что мы храним некоторые объекты в списке. Некоторое время спустя кто-то на самом деле понимает, что HashSet - гораздо лучшая альтернатива, например, из-за лучших характеристик поиска. Это когда мы можем попасть в беду. Для внутреннего использования List будет использовать для сравнения тип равенства по умолчанию, что в вашем случае означает Equals, а HashSet использует GetHashCode (). Если они ведут себя по-разному, то и ваша программа тоже. И имейте в виду, что такие проблемы не легко устранить.

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


0

Как .NET 4.7предпочтительный способ переопределения GetHashCode()показан ниже. Если вы нацелены на более старые версии .NET, включите пакет nuget System.ValueTuple .

// C# 7.0+
public override int GetHashCode() => (FooId, FooName).GetHashCode();

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


-1

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

РЕДАКТИРОВАНИЕ: Это было неправильно, оригинальный метод GetHashCode () не может гарантировать равенство 2 значений. Хотя равные объекты возвращают один и тот же хэш-код.


-6

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

    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;  
    }

12
Ожидается, что реализация GetHashCode () будет очень легкой. Я не уверен, что использование отражения заметно при использовании StopWatch на тысячах вызовов, но это, безусловно, на миллионах (подумайте о заполнении словаря из списка).
Богдан_троценко
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.