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

1212

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

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?

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

12 ответов

1192
Лучший ответ

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


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

  • 39
    Могу я спросить, а умножаете ли вы эти факторы?
  • 18
    На самом деле, я мог бы потерять одного из них; смысл в том, чтобы попытаться свести к минимуму количество столкновений - чтобы объект {1,0,0} имел хеш-код, отличающийся от {0,1,0} и {0,0,1} (если вы понимаете, о чем я ),
Показать ещё 24 комментария
117

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

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

  • 0
    Я не думаю, что это очень трудно реализовать, есть код. учитывая эти правила и лучше объяснил в эффективной книге C #, я думаю, что переопределить GetHashCode довольно легко.
  • 3
    Можете ли вы уточнить, что "хеш-код не должен изменяться в течение жизни объекта"? Этот NHibernate специфичен?
Показать ещё 10 комментариев
47

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

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

  • 6
    Разве это не всегда возвращает ноль? Вероятно, следует инициализировать результат 1! Также нужно еще несколько точек с запятой.
  • 14
    Вы знаете, что делает оператор XOR (^)?
Показать ещё 3 комментария
32

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

  • 6
    Эта проверка на тип не будет выполнена в ситуации, когда подкласс ссылается на метод Equals суперкласса как часть собственного сравнения (т. Е. Base.Equals (obj)) - следует использовать вместо него
  • 0
    @sweetfa: Это зависит от того, как реализован метод Equals подкласса. Он также может вызвать base.Equals ((BaseType) obj)), который будет работать нормально.
Показать ещё 4 комментария
26

Как насчет:

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

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

  • 1
    эм - но вы возвращаете строку для метода, основанного на int; _0
  • 29
    Нет, он вызывает GetHashCode () из объекта String, который возвращает int.
Показать ещё 6 комментариев
9

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

8

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

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

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

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

8

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

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

  • Если кто-то помещает ваш объект в коллекцию, которая вызывает 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");
    }
}
  • 4
    Создание исключения из GetHashCode является нарушением контракта объекта. Нетрудно определить функцию GetHashCode , чтобы любые два равных объекта возвращали один и тот же хэш-код; return 24601; и return 8675309; оба будут действительными реализациями GetHashCode . Производительность Dictionary будет приличной только тогда, когда количество элементов невелико, и станет очень плохой, если количество элементов увеличится, но в любом случае будет работать правильно.
  • 2
    @supercat, Невозможно разумно реализовать GetHashCode, если поля идентификации в объекте могут измениться, так как хеш-код никогда не должен изменяться. Выполнение того, что вы говорите, может привести к тому, что кому-то придется потратить много дней на то, чтобы отследить проблему с производительностью, а затем на недели, чтобы перепроектировать большую систему, чтобы исключить использование словарей.
Показать ещё 4 комментария
5

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

4

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

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

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

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

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

-5

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню