Поскольку я не смог найти ответ, который объясняет, почему мы должны переопределять GetHashCode
и Equals
для пользовательских структур и почему реализация по умолчанию "вряд ли подойдет для использования в качестве ключа в хэш-таблице", я оставлю ссылку на этот блог Почта , которое объясняет, почему с реальным примером проблемы, которая произошла.
Я рекомендую прочитать весь пост, но вот резюме (выделение и пояснения добавлены).
Причина, по которой хэш по умолчанию для структур является медленным и не очень хорошим:
Как устроен CLR, каждый вызов члена, определенного в System.ValueType
или System.Enum
типа [может] вызывать распределение бокса [...]
Реализация хеш-функции стоит перед дилеммой: правильно распределить хеш-функцию или сделать ее быстрой. В некоторых случаях, можно добиться их обоих, но это трудно сделать это в общем в ValueType.GetHashCode
.
Каноническая хеш-функция структуры «объединяет» хеш-коды всех полей. Но единственный способ получить хеш-код поля в ValueType
методе - это использовать отражение . Итак, авторы CLR решили обменивать скорость на дистрибутив, а GetHashCode
версия по умолчанию просто возвращает хеш-код первого ненулевого поля и «подставляет» его с идентификатором типа [...]. Это разумное поведение, если только , Например, если вам не повезло, и первое поле вашей структуры имеет одинаковое значение для большинства экземпляров, то хеш-функция будет постоянно показывать один и тот же результат . И, как вы можете себе представить, это приведет к значительному снижению производительности, если эти экземпляры будут храниться в хэш-наборе или хэш-таблице.
[...] Реализация на основе отражений медленная . Очень медленно.
[...] Оба ValueType.Equals
и ValueType.GetHashCode
имеют специальную оптимизацию. Если тип не имеет «указателей» и правильно упакован [...], то используются более оптимальные версии: GetHashCode
перебирает экземпляр и блоки XOR по 4 байта, а Equals
метод сравнивает два экземпляра, используя memcmp
. [...] Но оптимизация очень сложная. Во-первых, трудно понять, когда включена оптимизация [...] Во-вторых, сравнение памяти не обязательно даст вам правильные результаты . Вот простой пример: [...] -0.0
и +0.0
равны, но имеют разные двоичные представления.
Реальная проблема, описанная в посте:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Мы использовали кортеж, который содержал пользовательскую структуру с реализацией равенства по умолчанию. И, к сожалению, структура имела необязательное первое поле, которое почти всегда равнялось [пустой строке] . Производительность была в порядке, пока количество элементов в наборе значительно не увеличилось, что привело к реальной проблеме производительности, и потребовались минуты, чтобы инициализировать коллекцию из десятков тысяч элементов.
Итак, чтобы ответить на вопрос «в каких случаях я должен упаковать свою собственную, и в каких случаях я могу смело полагаться на реализацию по умолчанию», по крайней мере, в случае структур , вы должны переопределить Equals
и GetHashCode
всякий раз, когда ваша пользовательская структура может использоваться как введите хэш-таблицу или Dictionary
.
Я также рекомендовал бы реализовать IEquatable<T>
в этом случае, чтобы избежать бокса.
Как и в других ответах, если вы пишете класс , хэш по умолчанию, использующий равенство ссылок, обычно подходит, поэтому я не буду беспокоиться в этом случае, если вам не нужно переопределять Equals
(тогда вам придется переопределять GetHashCode
соответственно).