Поскольку я не смог найти ответ, который объясняет, почему мы должны переопределять 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соответственно).