Оскільки я не зміг знайти відповідь, яка пояснює, чому нам слід переосмислити 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
відповідно переосмислити ).