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

15 ответов

Решение

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

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

По состоянию на .NET 4.7 предпочтительный метод переопределения GetHashCode()показано ниже. Если нацелены на более старые версии.NET, включите пакет Nuget System.ValueTuple.

// C# 7.0+
public override int GetHashCode() => (FooId, FooName).GetHashCode();

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

Как насчет:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Начиная с C# 9(.net 5 или .net core 3.1), вы можете использовать записи, как это делает равенство на основе значений .

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

Например, предположим, что мы храним некоторые объекты в списке. Некоторое время спустя кто-то действительно понимает, что HashSet - намного лучшая альтернатива, например, из-за лучших характеристик поиска. Вот тогда мы можем попасть в беду. List будет внутренне использовать компаратор равенства по умолчанию для типа, что означает Equals в вашем случае, в то время как HashSet использует GetHashCode(). Если они ведут себя по-разному, ваша программа тоже. И имейте в виду, что такие проблемы не так просто устранить.

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

Насколько я понимаю, оригинальный 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;  
    }
Другие вопросы по тегам