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



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

646   12  

12 ответов:

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

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

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

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

Comments

    Ничего не найдено.