Правила GetHashCode в C #


137

Я прочитав у книзі Essential C # 3.0 та .NET 3.5, що:

Повернення GetHashCode () протягом життя певного об'єкта повинно бути постійним (однакове значення), навіть якщо дані об'єкта змінюються. У багатьох випадках для забезпечення цього слід кешувати метод return.

Це дійсне керівництво?

Я спробував кілька вбудованих типів у .NET, і вони не поводились так.


Ви можете розглянути можливість зміни прийнятої відповіді, якщо це можливо.
Giffyguy

Відповіді:


94

Відповідь головним чином - це дійсний орієнтир, але, можливо, не дійсне правило. Це також не розповідає всієї історії.

Суть полягає в тому, що для змінних типів ви не можете засновувати хеш-код на даних, що змінюються, оскільки два рівні об'єкти повинні повертати один і той же хеш-код, і хеш-код повинен бути дійсним протягом усього життя об'єкта. Якщо хеш-код зміниться, ви отримаєте об'єкт, який загубиться у хешованій колекції, оскільки він більше не живе у правильному хеш-коді.

Наприклад, об'єкт A повертає хеш 1. Таким чином, він потрапляє до кошика 1 хеш-таблиці. Потім ви змінюєте об'єкт A таким, що він повертає хеш 2. Коли хеш-таблиця шукає його, він виглядає у контейнері 2 і не може його знайти - об'єкт осиротів у контейнері 1. Ось чому хеш-код повинен не змінюється протягом життя об’єкта , і лише одна з причин, чому написання реалізацій GetHashCode - це біль у зад.

Оновлення
Ерік Ліпперт опублікував свій блог, який містить чудову інформацію про GetHashCode.

Додаткове оновлення
Я вніс кілька змін вище:

  1. Я розрізнив керівництво та правило.
  2. Я пробив "протягом усього життя об'єкта".

Настанова - це просто настанова, а не правило. Насправді GetHashCodeслід дотримуватися цих вказівок лише тоді, коли речі очікують, що об'єкт буде дотримуватися вказівок, наприклад, коли він зберігається в хеш-таблиці. Якщо ви ніколи не збираєтеся використовувати свої об'єкти в хеш-таблицях (або що-небудь інше, що спирається на правила GetHashCode), ваша реалізація не повинна відповідати інструкціям.

Коли ви бачите "за весь час існування об'єкта", вам слід прочитати "про час, який об'єкт повинен співпрацювати з хеш-таблицями" або подібне. Як і більшість речей, GetHashCodeйдеться про те, щоб знати, коли слід порушувати правила.


1
Як ви визначаєте рівність між змінними типами?
Джон Б

9
Ви не повинні використовувати GetHashCode для визначення рівності.
JSB,

4
@JS Bangs - З MSDN: похідні класи, які замінюють GetHashCode, повинні також замінити Equals, щоб гарантувати, що два об'єкти, які вважаються рівними, мають однаковий хеш-код; інакше тип Hashtable може працювати неправильно.
Джон Б,

3
@ Джоан Венге: Дві речі. По-перше, навіть Microsoft не отримує GetHashCode прямо при кожному впровадженні. По-друге, типи значень, як правило, незмінні, причому кожне значення є новим екземпляром, а не модифікацією існуючого екземпляра.
Джефф Йейтс,

17
Оскільки a.Equals (b) має означати, що a.GetHashCode () == b.GetHashCode (), хеш-код найчастіше доводиться змінювати, якщо змінюються дані, що використовуються для порівняння рівності. Я б сказав, що проблема не в тому, що GetHashCode базується на змінних даних. Проблема полягає у використанні змінних об’єктів як ключів хеш-таблиці (і насправді їх мутації). Я помиляюся?
Ніклас

121

Минуло багато часу, але, тим не менше, я вважаю, що все-таки необхідно дати правильну відповідь на це питання, включаючи пояснення про те, чому та як. На сьогодні найкраща відповідь - це вичерпне посилання на MSDN - не намагайтеся складати власні правила, хлопці з MS знали, що роблять.

Але перш за все: Настанова, як цитується у питанні, є неправильною.

Тепер чому - їх двоє

По-перше, чому : якщо хеш-код обчислюється таким чином, що він не змінюється протягом життя об'єкта, навіть якщо сам об'єкт змінюється, тоді він порушить рівноправний контракт.

Пам'ятайте: "Якщо два об'єкти порівнюються як рівні, метод GetHashCode для кожного об'єкта повинен повертати одне і те ж значення. Однак, якщо два об'єкти не порівнюються як рівні, методи GetHashCode для двох об'єктів не повинні повертати різні значення."

Друге речення часто неправильно трактується як "Єдине правило полягає в тому, що під час створення об'єкта хеш-код рівних об'єктів повинен бути рівним". Насправді не знаю, чому, але це приблизно суть більшості відповідей тут.

Подумайте про два об’єкти, що містять ім’я, де ім’я використовується в методі equals: Те саме ім’я -> одне і те ж. Створити екземпляр A: Name = Joe Створити екземпляр B: Name = Peter

Хеш-код A і Hashcode B, швидше за все, будуть не однаковими. Що станеться тепер, коли ім'я екземпляра B зміниться на Joe?

Відповідно до настанови із запитання, хеш-код B не змінився. Результатом цього буде: A.Equals (B) ==> true Але одночасно: A.GetHashCode () == B.GetHashCode () ==> false.

Але саме така поведінка явно заборонена рівними та хеш-кодами.

По-друге, чому . Хоча це - звичайно - правда, що зміни в хеш-коді можуть порушити хешовані списки та інші об’єкти, що використовують хеш-код, також є зворотне. Якщо не змінити хеш-код, то в гіршому випадку будуть отримані хешовані списки, де всі багато різних об'єктів матимуть однаковий хеш-код, і тому вони будуть знаходитися в одному хеш-коді - це відбувається, коли об'єкти ініціалізуються зі стандартним значенням, наприклад.


Зараз наближається до хау. Ну, на перший погляд, здається, існує суперечність - в будь-якому випадку, код зламається. Але жодна проблема не походить від зміненого або незмінного хеш-коду.

Джерело проблем добре описано в MSDN:

З запису хеш-таблиці MSDN:

Ключові об'єкти повинні бути незмінними до тих пір, поки вони використовуються як ключі в хеш-таблиці.

Це означає:

Будь-який об'єкт, який створює хешвале, повинен змінити хешвайлу, коли об'єкт змінюється, але він не повинен - ​​абсолютно не повинен - ​​допускати будь-яких змін до себе, коли він використовується всередині хеш-таблиці (або будь-якого іншого об'єкта, що використовує хеш, звичайно) .

По-перше, як найпростіший спосіб, звичайно, був би проектувати незмінні об'єкти лише для використання в хештегах, які будуть створюватися як копії звичайних, змінних об'єктів, коли це буде потрібно. Всередині незмінних об'єктів, очевидно, добре кешувати хеш-код, оскільки він незмінний.

По-друге, як або дати об’єкту прапорець «ви хешуєте зараз», переконатися, що всі дані об’єкта є приватними, перевірити прапорець у всіх функціях, які можуть змінювати дані об’єктів, і викидати дані винятків, якщо зміни не дозволено (тобто встановлено прапор ). Тепер, коли ви поміщаєте об'єкт у будь-яку хешовану область, переконайтеся, що встановили прапор, а також - також зняли прапор, коли він більше не потрібен. Для зручності використання я б порадив встановити прапор автоматично всередині методу "GetHashCode" - таким чином про нього не можна забувати. І явний виклик методу "ResetHashFlag" переконається, що програмісту доведеться думати, чи дозволено чи не можна змінювати дані об'єктів на даний момент.

Гаразд, що слід сказати також: Є випадки, коли можна мати об’єкти із змінними даними, коли хеш-код, тим не менше, не змінюється, коли дані об’єктів змінюються, не порушуючи дорівнює & hashcode-контракт.

Однак це вимагає, щоб метод equals також не базувався на змінних даних. Отже, якщо я пишу об'єкт і створюю метод GetHashCode, який обчислює значення лише один раз і зберігає його всередині об'єкта, щоб повернути його при подальших викликах, то я, знову ж таки: абсолютно повинен, створити метод Equals, який буде використовувати збережені значення для порівняння, так що A.Equals (B) також ніколи не зміниться з false на true. Інакше контракт був би порушений. Результатом цього, як правило, є те, що метод Equals не має жодного сенсу - це не оригінальне посилання дорівнює, але не є рівним і значення. Іноді це може бути передбачувана поведінка (тобто записи клієнтів), але зазвичай це не так.

Отже, просто зробіть зміну результату GetHashCode, коли дані об’єкта змінюються, і якщо використання об’єкта всередині хешу за допомогою списків або об’єктів призначене (або просто можливе), то зробіть об’єкт незмінним або створіть прапор лише для читання, щоб використовувати для час життя хешованого списку, що містить об'єкт.

(До речі: все це не є специфічним для C # oder .NET - це характер усіх реалізацій хеш-таблиць або, загальніше, будь-якого індексованого списку, що ідентифікаційні дані об'єктів ніколи не повинні змінюватися, поки об'єкт знаходиться у списку . Якщо це правило порушено, трапиться несподівана та непередбачувана поведінка. Десь можуть бути реалізації списків, які контролюють усі елементи у списку та здійснюють автоматичне переіндексування списку, але продуктивність цих, безумовно, буде в найкращому випадку жахливою.)


23
+1 за це детальне пояснення (дав би більше, якби міг)
Олівер

5
+1 це, безумовно, краща відповідь через багатослівне пояснення! :)
Джо

9

З MSDN

Якщо два об'єкти порівнюються як рівні, метод GetHashCode для кожного об'єкта повинен повертати одне і те ж значення. Однак, якщо два об'єкти не порівнюються як рівні, методи GetHashCode для двох об'єктів не повинні повертати різні значення.

Метод GetHashCode для об'єкта повинен послідовно повертати той самий хеш-код, доки не буде модифікації стану об'єкта, що визначає повернене значення методу Equals об'єкта. Зверніть увагу, що це справедливо лише для поточного виконання програми, і що інший хеш-код можна повернути, якщо додаток буде запущено знову.

Для найкращої роботи хеш-функція повинна генерувати випадковий розподіл для всіх введених даних.

Це означає, що якщо значення об’єкта змінюється, хеш-код повинен змінитися. Наприклад, клас "Person" із властивістю "Name", встановленою на "Tom", повинен мати один хеш-код та інший код, якщо ви зміните ім'я на "Jerry". В іншому випадку Том == Джеррі, що, мабуть, зовсім не те, що ви б призначили.


Редагувати :

Також від MSDN:

Похідні класи, які замінюють GetHashCode, повинні також перевизначити Equals, щоб гарантувати, що два об'єкти, які вважаються рівними, мають однаковий хеш-код; інакше тип Hashtable може працювати неправильно.

З запису хеш-таблиці MSDN :

Ключові об'єкти повинні бути незмінними до тих пір, поки вони використовуються як ключі в хеш-таблиці.

Я читав це так, що змінні об'єкти повинні повертати різні хеш-коди, коли їх значення змінюються, якщо вони не призначені для використання в хеш-таблиці.

У прикладі System.Drawing.Point, об'єкт є змінним, і робить повертати різний хеш - код при зміні значення X або Y. Це зробило б поганим кандидатом використання, як є в хеш-таблиці.


GetHashCode () призначений для використання в хеш-таблиці, це єдиний пункт цієї функції.
skolima

@skolima - документація MSDN несумісна з цим. Змінні об'єкти можуть реалізовувати GetHashCode () і повинні повертати різні значення при зміні значення об'єкта. Hashtables повинні використовувати незмінні ключі. Отже, ви можете використовувати GetHashCode () для чогось іншого, крім хеш-таблиці.
Джон Б,

9

Я думаю, що документація щодо GetHashcode трохи заплутана.

З одного боку, MSDN заявляє, що хеш-код об'єкта ніколи не повинен змінюватися і бути постійним. З іншого боку, MSDN також заявляє, що повертане значення GetHashcode має бути рівним для 2 об'єктів, якщо ці 2 об'єкти вважаються рівними.

MSDN:

Хеш-функція повинна мати такі властивості:

  • Якщо два об'єкти порівнюються як рівні, метод GetHashCode для кожного об'єкта повинен повертати одне і те ж значення. Однак, якщо два об'єкти не порівнюються як рівні, методи GetHashCode для двох об'єктів не повинні повертати різні значення.
  • Метод GetHashCode для об'єкта повинен послідовно повертати той самий хеш-код, доки не буде модифікації стану об'єкта, що визначає повернене значення методу Equals об'єкта. Зверніть увагу, що це справедливо лише для поточного виконання програми, і що інший хеш-код можна повернути, якщо додаток буде запущено знову.
  • Для найкращої роботи хеш-функція повинна генерувати випадковий розподіл для всіх введених даних.

Тоді це означає, що всі ваші об'єкти повинні бути незмінними, або метод GetHashcode повинен базуватися на властивостях вашого об'єкта, які незмінні. Припустимо, наприклад, що у вас є цей клас (наївна реалізація):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

Ця реалізація вже порушує правила, які можна знайти в MSDN. Припустимо, у вас є 2 екземпляри цього класу; для властивості Name instance1 встановлено значення 'Pol', а для властивості Name instance2 встановлено значення 'Piet'. Обидва екземпляри повертають інший хеш-код, і вони також не рівні. Тепер, припустимо, що я змінив ім'я instance2 на 'Pol', тоді, згідно мого методу Equals, обидва екземпляри повинні бути рівними, і згідно з одним із правил MSDN, вони повинні повернути один і той же хеш-код.
Однак цього зробити не можна, оскільки хеш-код instance2 зміниться, а MSDN заявляє, що це заборонено.

Тоді, якщо у вас є сутність, ви можете, можливо, реалізувати хеш-код, щоб він використовував "основний ідентифікатор" цієї сутності, який, можливо, в ідеалі є сурогатним ключем або незмінною властивістю. Якщо у вас є об'єкт значення, ви можете реалізувати хеш-код, щоб він використовував "властивості" цього об'єкта значення. Ці властивості складають "визначення" об'єкта значення. Звичайно, це природа ціннісного об’єкта; вас цікавить не його ідентичність, а скоріше її цінність.
І, отже, об’єкти значення повинні бути незмінними. (Так само, як і в середовищі .NET, рядок, дата тощо ... - усі незмінні об’єкти).

Ще одне, що спадає на думку:
під час якого „сеансу” (я не знаю, як насправді це називати) слід „GetHashCode” повертати постійне значення. Припустимо, ви відкриваєте свою програму, завантажуєте екземпляр об’єкта з БД (сутності) і отримуєте його хеш-код. Він поверне певну кількість. Закрийте програму та завантажте ту саму сутність. Чи потрібно, щоб хеш-код цього разу мав те саме значення, що і при першому завантаженні сутності? ІМХО, ні.


1
Вашим прикладом є те, чому Джефф Йейтс каже, що ви не можете засновувати хеш-код на змінних даних. Ви не можете вставити змінний об’єкт у словник і очікувати, що він буде добре працювати, якщо хеш-код базується на змінних значеннях цього об’єкта.
Ogre Psalm33,

3
Я не можу зрозуміти, де порушено правило MSDN? Правило чітко говорить: метод GetHashCode для об'єкта повинен послідовно повертати той самий хеш-код, доки не буде модифікації стану об'єкта, що визначає значення повернення методу Equals об'єкта . Це означає, що хеш-код instance2 можна змінювати при зміні Імені instance2 на Pol
chikak

8

Це хороша порада. Ось що з цього приводу має сказати Брайан Пепін:

Це мене спотикало не раз: переконайтесь, що GetHashCode завжди повертає одне і те ж значення протягом усього періоду екземпляра. Пам'ятайте, що хеш-коди використовуються для ідентифікації "сегментів" у більшості реалізацій хеш-таблиць. Якщо "відро" об'єкта змінюється, хеш-таблиця може не вдатися знайти ваш об'єкт. Це можуть бути дуже важкі помилки, тому виправте їх із першого разу.


Я не проголосував проти, але я здогадуюсь, що інші зробили це, бо це цитата, яка не охоплює всієї проблеми. Прикидаються рядки, які можна змінювати, але не змінювали хеш-коди. Ви створюєте "bob", використовуєте його як ключ у хеш-таблиці, а потім змінюєте його значення на "phil". Далі створіть новий рядок "phil". якщо ви потім шукаєте запис хеш-таблиці з ключем "phil", елемент, який ви спочатку ввели, не буде знайдений. Якби хтось шукав на "bob", його було б знайдено, але ви отримали б значення, яке, можливо, більше не буде правильним. Або будьте старанними, щоб не використовувати клавіші, що змінюються, або пам’ятайте про небезпеку.
Eric Tuttleman

@EricTuttleman: Якби я писав правила для фреймворку, я б вказав, що для будь-якої пари об'єктів Xі Y, коли X.Equals(Y)або Y.Equals(X)буде викликаний, усі майбутні дзвінки повинні давати однаковий результат. Якщо ви хочете використати інше визначення рівності, використовуйте EqualityComparer<T>.
supercat

5

Не відповідаючи безпосередньо на ваше запитання, але - якщо ви використовуєте Resharper, не забувайте, що він має функцію, яка генерує розумну реалізацію GetHashCode (а також метод Equals) для вас. Звичайно, ви можете вказати, які члени класу будуть враховані при обчисленні хеш-коду.


Дякую, насправді я ніколи не використовував Resharper, але я постійно бачу, як про нього згадують досить часто, тому мені слід спробувати.
Джоан Венге

+1 Resharper, якщо він є, генерує приємну реалізацію GetHashCode.
ΩmegaMan

5

Перегляньте цю публікацію в блозі від Марка Брукса:

VTO, RTO та GetHashCode () - о, боже!

А потім перевірте наступний пост (не можу посилатись, оскільки я новачок, але посилання є в статті initlal), де далі обговорюються та висвітлюються деякі незначні недоліки в початковій реалізації.

Це було все, що мені потрібно було знати про створення реалізації GetHashCode (), він навіть забезпечує завантаження свого методу разом з деякими іншими утилітами, коротше золото.


4

Хеш-код ніколи не змінюється, але також важливо розуміти, звідки береться хеш-код.

Якщо ваш об’єкт використовує семантику значень, тобто ідентичність об’єкта визначається його значеннями (як String, Color, усі структури). Якщо ідентичність вашого об'єкта не залежить від усіх його значень, тоді хеш-код ідентифікується підмножиною його значень. Наприклад, ваш запис StackOverflow десь зберігається в базі даних. Якщо ви змінили своє ім’я чи електронну адресу, запис вашого клієнта залишається незмінним, хоча деякі значення змінилися (зрештою, вас зазвичай ідентифікують за довгим ідентифікатором клієнта).

Отож коротко:

Семантика типу значення - Геш-код визначається значеннями Семантика посилального типу - Геш-код визначається деяким ідентифікатором

Пропоную вам прочитати Дизайн домену Еріком Евансом, де він переходить до сутностей проти типів цінностей (що більш-менш те, що я намагався зробити вище), якщо це все ще не має сенсу.


Це насправді не правильно. Хеш-код повинен залишатися незмінним для конкретного екземпляра. У випадку типів значень часто трапляється так, що кожне значення є унікальним екземпляром, і тому, здається, хеш змінюється, але насправді є новим екземпляром.
Джефф Йейтс,

Ви маєте рацію, типи значень незмінні, тому вони виключають зміну. Хороший улов.
DavidN

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.