Оскільки я не зміг знайти відповідь, яка пояснює, чому нам слід переосмислити GetHashCodeі Equalsдля користувацьких структур, і чому реалізація за замовчуванням "не може бути придатною для використання в якості ключа в хеш-таблиці", я залишу посилання на цей блог повідомлення , в якому пояснюється, чому на прикладі реальної справи проблеми, яка сталася.
Рекомендую прочитати весь пост, але ось резюме (акценти та уточнення додано).
Причина, що хеш за замовчуванням для структур є повільним і не дуже хорошим:
Спосіб проектування CLR, кожен дзвінок члену, визначеному System.ValueTypeабо System.Enumтипів [може] викликати розподіл боксу [...]
Реалізатор хеш-функції стоїть перед дилемою: зробити хороший розподіл хеш-функції або зробити її швидкою. У деяких випадках, можна домогтися їх обох, але це важко зробити це в загальному в ValueType.GetHashCode.
Канонічна хеш-функція структури "поєднує" хеш-коди всіх полів. Але єдиний спосіб отримати хеш-код поля в ValueTypeметоді - використовувати відображення . Отже, автори CLR вирішили торгувати швидкістю над розподілом, а GetHashCodeверсія за замовчуванням просто повертає хеш-код першого ненульового поля та "розміщує" його з ідентифікатором типу [...] Це розумна поведінка, якщо це не . Наприклад, якщо вам не пощастило і перше поле вашої структури має однакове значення для більшості екземплярів, то хеш-функція буде забезпечувати однаковий результат весь час. І, як ви можете собі уявити, це призведе до різкого впливу на продуктивність, якщо ці екземпляри зберігатимуться в хеш-наборі або хеш-таблиці.
[...] Реалізація на основі роздумів повільна . Дуже повільно.
[...] Обидва ValueType.Equalsі ValueType.GetHashCodeмають особливу оптимізацію. Якщо тип не має "покажчиків" і упакований належним чином [...], використовуються більш оптимальні версії: GetHashCodeітерація над екземпляром та блоками XORs у 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відповідно переосмислити ).