Поскольку я не смог найти ответ, который объясняет почему , мы должны переопределить 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; }
}
Мы использовали кортеж, который содержал пользовательскую структуру с реализацией равенства по умолчанию. И, к сожалению, структура имела необязательное первое поле, которое почти всегда равнялось [пустой строке] . Производительность была в порядке, пока количество элементов в наборе значительно не увеличилось, что привело к реальной проблеме производительности, и потребовались минуты, чтобы инициализировать коллекцию из десятков тысяч элементов.
Итак, чтобы ответить на вопрос «в каких случаях я должен упаковать свою собственную, и в каких случаях я могу смело полагаться на реализацию по умолчанию», по крайней мере, в случае Structs , вы должны переопределить Equals
и GetHashCode
всякий раз, когда ваша пользовательская структура может использоваться в качестве ключа в хэш-таблице или Dictionary
.
Я бы также рекомендовал использовать IEquatable<T>
в этом случае, чтобы избежать бокса.
Как и в других ответах, если вы пишете класс , хэш по умолчанию, использующий равенство ссылок, обычно подходит, поэтому я не буду беспокоиться в этом случае, , если вам нужно переопределить Equals
(тогда вам придется переопределить GetHashCode
соответственно).