Почему важно переопределить GetHashCode, если переопределен метод Equals? - PullRequest
1313 голосов
/ 16 декабря 2008

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

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 s. Какой метод является предпочтительным для переопределения GetHashCode?

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

Ответы [ 12 ]

1219 голосов
/ 16 декабря 2008

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


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

127 голосов
/ 21 декабря 2008

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

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

52 голосов
/ 16 декабря 2008

Переопределяя 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;
    }
}

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

37 голосов
/ 17 ноября 2011

Пожалуйста, не забудьте проверить параметр 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

28 голосов
/ 25 ноября 2010

Как насчет:

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

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

10 голосов
/ 12 ноября 2014

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

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

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

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

10 голосов
/ 19 ноября 2013

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

  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");
    }
}
10 голосов
/ 16 декабря 2008

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

8 голосов
/ 21 февраля 2012

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

6 голосов
/ 27 июня 2011

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

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

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

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


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

...