Переопределение GetHashCode()

В этой статье Джон Скит упомянул, что обычно использует такой алгоритм для переопределения GetHashCode().

public override int GetHashCode()
{
  unchecked // Overflow is fine, just wrap
  {
    int hash = 17;
    // Suitable nullity checks etc, of course :)
    hash = hash * 23 + Id.GetHashCode();
    return hash;
  }
}

Теперь я попытался использовать это, но Resharper сообщает мне, что метод GetHashCode() должен быть хешированием, используя только поля только для чтения (он компилируется отлично, хотя). Что было бы хорошей практикой, потому что прямо сейчас я не могу иметь свои поля только для чтения?

Я попытался создать этот метод с помощью Resharper, вот результат.

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

Это не способствует, если честно...

Ответ 1

Если все ваши поля изменяемы, и вы должны реализовать метод GetHashCode, я боюсь, что это будет ваша реализация.

public override int GetHashCode() 
{ 
    return 1; 
} 

Да, это неэффективно, но это по крайней мере правильно.

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

Обратите внимание, что при всех объектах, возвращающих один и тот же HashCode 1, это в основном означает, что все объекты помещаются в одно и то же ведро в HashSet или Dictionary. Таким образом, в HashSet или Dictionary всегда есть только одно ведро. При попытке поиска объекта он проверит проверку равенства на каждом из объектов внутри единственного ведра. Это похоже на поиск в связанном списке.

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

Ответ 2

Обратите внимание, что ваш GetHashCode должен идти рука об руку с вашим методом Equals. И если вы можете просто использовать ссылочное равенство (когда у вас никогда не будет двух разных экземпляров вашего класса, которые могут быть равны), вы можете безопасно использовать Equals и GetHashCode, которые унаследованы от Object. Это будет работать намного лучше, чем просто return 1 из GetHashCode.

Ответ 3

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

Например

public class A
{
    // TODO Equals override

    public override int GetHashCode()
    {
        return 21313;
    } 
}

public class B
{
    // TODO Equals override

    public override int GetHashCode()
    {
        return 35507;
    } 
}

Тогда, если у меня есть Dictionary<object, TValue> содержащий экземпляры A, B и других типов, производительность поиска будет лучше, чем если бы все реализации GetHashCode вернули одно и то же числовое значение.

Следует также отметить, что я использую простые числа, чтобы получить лучшее распределение.

В соответствии с комментариями я представил пример LINQPad здесь, который демонстрирует разницу в производительности между использованием return 1 для разных типов и возвратом другого значение для каждого типа.