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

StackOverflow https://stackoverflow.com/questions/371328

  •  21-08-2019
  •  | 
  •  

Вопрос

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

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?

Это было полезно?

Решение

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


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

Другие советы

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

Я, наконец, нашел решение этой проблемы, когда работал с NHibernate.Мой подход заключается в вычислении хэш-кода по идентификатору объекта.Идентификатор может быть установлен только через конструктор, поэтому, если вы хотите изменить идентификатор, что очень маловероятно, вам придется создать новый объект с новым идентификатором и, следовательно, с новым хэш-кодом.Этот подход лучше всего работает с идентификаторами 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)
{
    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

Как насчет:

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

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

Нам нужно справиться с двумя проблемами.

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

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

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

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

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

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

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

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

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

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

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

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

    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;  
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top