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

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

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?

12 ответов


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

на 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;

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


демонстрация того, что происходит, когда вы получаете это неправильно здесь.


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

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


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

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


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

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


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


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

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

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

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


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

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

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


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

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

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

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


   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 () возвращает адрес памяти объекта, поэтому важно переопределить его, если вы хотите сравнить два разных объекта.

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


ниже использование отражения кажется мне лучшим вариантом с учетом общедоступных свойств, так как с этим вам не нужно беспокоиться о добавлении / удалении свойств (хотя и не такой распространенный сценарий). Я обнаружил, что это тоже работает лучше.(Сравненное время используя секундомер 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;  
    }