Почему важно переопределить 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;

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

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

Ответ 1

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


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

Ответ 2

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

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

Ответ 3

Отвергая 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;
    }
}

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

Ответ 4

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

Ответ 5

Как насчет:

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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

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

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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

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

Ответ 12

когда gethashcode() имеет значение, если вы хотите расположить два объекта на себе. но если вам нужно разбить два объекта на основе их параметров, вы можете просто разбить параметр объектов друг на друга.

public class A{
    public string name;
    public override bool Equals(object obj)
    {
      if(obj!=null){
       return name == obj.name;
    }
}

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

Ответ 13

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

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