Чому важливо переосмислити GetHashCode, коли метод Equals буде замінено?


1444

Дано наступний клас

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null) 
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

Я змінив Equalsметод, тому що Fooпредставляє рядок для Fooтаблиці s. Який спосіб є кращим для переосмислення GetHashCode?

Чому важливо переосмислити GetHashCode?


36
Важливо реалізувати як рівний, так і gethashcode, через зіткнення, зокрема під час використання словників. якщо два об’єкти повертають один і той же хеш-код, вони вставляються в словник з ланцюжком. Під час доступу до елемента використовується метод дорівнює.
— DarthVader

Відповіді:


1319

Так, важливо, якщо ваш елемент буде використовуватися як ключ у словнику, або HashSet<T>тощо - оскільки це використовується (за відсутності спеціального користування IEqualityComparer<T>) для групування елементів у відрі. Якщо хеш-код для двох елементів не збігається, вони ніколи не можуть вважатися рівними ( Рівні просто ніколи не будуть називатися).

Метод GetHashCode () повинен відображати Equalsлогіку; правила такі:

  • якщо дві речі рівні ( Equals(...) == true), вони повинні повернути однакове значення дляGetHashCode()
  • якщо GetHashCode()рівні рівні, не потрібно, щоб вони були однаковими; це зіткнення, і Equalsбуде покликано перевірити, чи справжня це рівність чи ні.

У цьому випадку, схоже, що " return FooId;" є відповідною GetHashCode()реалізацією. Якщо ви тестуєте кілька властивостей, звичайно їх комбінувати за допомогою коду, як показано нижче, щоб зменшити діагональні зіткнення (тобто так, що new Foo(3,5)має інший хеш-код new Foo(5,3)):

unchecked // only needed if you're compiling with arithmetic checks enabled
{ // (the default compiler behaviour is *disabled*, so most folks won't need this)
    int hash = 13;
    hash = (hash * 7) + field1.GetHashCode();
    hash = (hash * 7) + field2.GetHashCode();
    ...
    return hash;
}

Ох - для зручності ви також можете розглянути питання про надання ==та !=операторів при переопределенні Equalsта GetHashCode.


Демонстрація того, що відбувається, коли ви помилитесь, є тут .


49
Чи можу я запитати, чи ти примножуєш такі фактори?
— Леандро Лопес

22
Насправді я, мабуть, могла втратити одну з них; справа в тому, щоб спробувати мінімізувати кількість зіткнень - щоб об’єкт {1,0,0} мав різний хеш до {0,1,0} і {0,0,1} (якщо ви бачите, що я маю на увазі ),
— Марк Гравелл

13
Я підправив цифри, щоб було зрозуміліше (і додав насіння). Деякий код використовує різні числа - наприклад, компілятор C # (для анонімних типів) використовує насіння 0x51ed270b і коефіцієнт -1521134295.
— Марк Гравелл

76
@ Леандро Лопес: Зазвичай фактори вибираються простими числами, оскільки це робить кількість зіткнень меншими.
— Андрій Ронеа

29
"О - для зручності, ви можете також розглянути можливість надання операторів == і! = При переході на рівні рівних та GethashCode.": Microsoft відмовляє в реалізації оператора == для об'єктів, які не змінюються - msdn.microsoft.com/en-us/library/ ms173147.aspx - " Недоцільно переосмислити оператор == у невідмінних типах."
— антидух

137

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

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


20
@vanja. Я вважаю, що це стосується: якщо ви додасте об'єкт до словника, а потім зміните ідентифікатор об'єкта, при отриманні пізніше ви будете використовувати інший хеш для його отримання, щоб ви ніколи не отримали його зі словника.
— ANeves

74
Документація Microsoft про функцію GetHashCode () Microsoft не заявляє і не передбачає, що хеш об'єктів повинен залишатися послідовним протягом усього життя. Насправді він спеціально пояснює один допустимий випадок, у якому він може не : "Метод GetHashCode для об'єкта повинен послідовно повертати один і той же хеш-код до тих пір, поки не буде модифіковано стан об'єкта, що визначає повернене значення методу рівняння об'єкта" . "
— PeterAllenWebb

37
"хеш-код не повинен змінюватися протягом життя об'єкта" - це неправда.
— апокаліпсис

7
Краще сказати, що "хеш-код (ні еваляція рівних) повинен змінюватися протягом періоду, коли об'єкт використовується як ключ для колекції". Отже, якщо ви додасте об'єкт до словника як ключ, ви повинні переконатися, що GetHashCode і Equals не змінять свої результати для заданого входу, поки ви не видалите об'єкт зі словника.
— Скотт Чемберлен

11
@ScottChamberlain Я думаю, ви забули НЕ у своєму коментарі, це повинно бути: "хеш-код (ні еваляція рівних) НЕ повинен змінюватися протягом періоду, коли об'єкт використовується як ключ для колекції". Правильно?
— Стен Прокоп

57

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

Це приклад того, як ReSharper пише функцію GetHashCode () для вас:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

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


7
Чи не завжди це поверне нуль? Можливо, слід ініціалізувати результат до 1! Також потрібно ще кілька напівколонок.
— Сем Макрілл

16
Вам відомо про те, що робить оператор XOR (^)?
— Стівен Дрю

1
Як я вже говорив, це те, про що пише R # (принаймні, те, що було зроблено ще в 2008 році), коли його попросили. Очевидно, цей фрагмент призначений для того, щоб певним чином підкоригувати програміст. Що стосується зниклих напівколонок ... так, схоже, я їх випустив, коли копіював вставлений код з вибору регіону у Visual Studio. Я також думав, що люди зрозуміють це і те, і інше.
— Пастка

3
@SamMackrill Я додав до відсутніх напівколонок.
— Меттью Мердок

5
@SamMackrill Ні, це не завжди буде повертати 0. 0 ^ a = a, так 0 ^ m_someVar1 = m_someVar1. Він міг би також встановити початкове значення resultдля m_someVar1.
— Міллі Сміт

41

Не забудьте перевірити параметр obj nullпри переопределенні Equals(). А також порівняйте тип.

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

Причина цього: Equalsповинна повертати помилкове порівняння з null. Дивіться також http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx


6
Ця перевірка на тип буде невдалою в ситуації, коли підклас посилається на метод надкласу рівних як частину власного порівняння (тобто base.Equals (obj)) - повинен використовуватись натомість
— sweetfa

@sweetfa: Це залежить від способу реалізації рівня підкласу. Він також може викликати base.Equals ((BaseType) obj)), який би працював нормально.
— huha

2
Ні, це не буде: msdn.microsoft.com/en-us/library/system.object.gettype.aspx . І крім того, реалізація методу не повинна провалюватися або успішно залежати від способу його виклику. Якщо тип об'єкта для виконання часу є підкласом деякого базового класу, то рівняння () базового класу повинно повернути істину, якщо вона objдійсно дорівнює, thisнезалежно від того, як називались рівними () базового класу.
— Юпітер

2
Переміщення fooItemдо вершини та перевірка її на нуль буде кращою у випадку нульового чи неправильного типу.
— IllidanS4 хоче, щоб Моніка повернулася

1
@ 40Alpha Ну, так, тоді obj as Fooбуло б недійсним.
— IllidanS4 хоче, щоб Моніка повернулася

35

Як щодо:

public override int GetHashCode()
{
    return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}

Якщо припустити, що це не проблема :)


1
erm - але ви повертаєте рядок для методу на основі int; _0
— jim tollan

32
Ні, він викликає GetHashCode () від об'єкта String, який повертає int.
— Річард Клейтон

3
Я не сподіваюся, що це буде так швидко, як я хотів би бути, не тільки для боксу, який бере участь у ціннісних типах, але і для продуктивності string.Format. Ще один видовищний, який я бачив, - це new { prop1, prop2, prop3 }.GetHashCode(). Не можу зауважити, хоча який із них буде повільніше. Не зловживайте інструментами.
— nawfal

16
Це поверне істину для { prop1="_X", prop2="Y", prop3="Z" }та { prop1="", prop2="X_Y", prop3="Z_" }. Напевно, цього не хочеш.
— voetsjoeba

2
Так, ви завжди можете замінити символ підкреслення на щось не таке поширене (наприклад, •, ▲, ►, ◄, ☺, ☻) і сподіваєтесь, що ваші користувачі не будуть використовувати ці символи… :)
— Людмил Тиньков

13

У нас є дві проблеми, з якими можна впоратися.

  1. Ви не можете надати розумного, GetHashCode()якщо будь-яке поле в об'єкті може бути змінено. Також часто об'єкт НІКОЛИ не буде використаний у колекції, від якої залежить GetHashCode(). Тож витрати на реалізацію GetHashCode()часто не варті, або це неможливо.

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

Тому я за замовчуванням роблю.

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null)
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}

5
Викидання виключення з GetHashCode - це порушення договору на об'єкт. Немає труднощів із визначенням такої GetHashCodeфункції, що будь-які два об'єкти, які є рівними, повертають один і той же хеш-код; return 24601;і return 8675309;обидва були б дійсними реалізаціями GetHashCode. Продуктивність Dictionaryбуде пристойною лише тоді, коли кількість предметів невелика, і вийде дуже погано, якщо кількість предметів стане великим, але воно буде працювати коректно в будь-якому випадку.
— supercat

2
@supercat, Неможливо реалізувати GetHashCode розумним чином, якщо ідентифікаційні поля в об'єкті можуть змінитися, оскільки хеш-код ніколи не повинен змінюватися. Виконання того, що ви говорите, може призвести до того, що комусь доведеться витратити багато днів на пошук проблеми з ефективністю, а потім багато тижнів на великій переробці системи, щоб усунути використання словників.
— Ян Рінроуз

2
Раніше я робив щось подібне для всіх класів, які я визначив, що потрібні рівні (), і де я був повністю впевнений, що ніколи не буду використовувати цей об'єкт як ключ у колекції. Потім одного дня програма, де я використав такий предмет як вхід до керування DevExpress XtraGrid, зазнала краху. Виявляється, XtraGrid за моєю спиною створював HashTable чи щось на базі моїх об’єктів. Я зіткнувся з другорядним аргументом із підтримкою DevExpress щодо цього. Я сказав, що це не розумно, що вони базували функціональність і надійність свого компонента на невідомому впровадженні клієнта незрозумілого методу.
— RenniePet

Люди DevExpress були досить химерними, в основному кажучи, що я повинен бути ідіотів, щоб кинути виняток у методі GetHashCode (). Я все ще думаю, що вони повинні знайти альтернативний метод робити те, що вони роблять - я пригадую Марка Гравелла в іншому потоці, який описує, як він будує словник довільних об'єктів, не будучи залежним від GetHashCode () - не можу згадати, як він це робив хоч.
— RenniePet

4
@RenniePet, має бути кращим пригніченням через викид винятку, а потім дуже важко знайти помилку через недійсну реалізацію.
— Ян Рінроуз

12

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


11

Просто додати відповіді вище:

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

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

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


8

Хеш-код використовується для колекцій на основі хешів, таких як словник, Hashtable, HashSet тощо. Метою цього коду є дуже швидке попереднє впорядкування конкретного об'єкта шляхом його розміщення в певну групу (відро). Це попереднє сортування надзвичайно допомагає знайти цей об’єкт, коли вам потрібно повернути його з колекції хешів, оскільки код повинен шукати ваш об’єкт лише в одному відрі, а не в усіх об'єктах, які він містить. Чим кращий розподіл хеш-кодів (краща унікальність), тим швидше пошук. В ідеальній ситуації, коли кожен об’єкт має унікальний хеш-код, виявлення це операція O (1). У більшості випадків воно наближається до O (1).


7

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

   public override int GetHashCode()
   {
      return base.GetHashCode();
   }

(Звичайно, я можу використовувати #pragma, щоб також відключити попередження, але я вважаю за краще цей спосіб.)

Коли ви перебуваєте в позиції , що ви дійсно потребуєте продуктивності , ніж всі питання , згадані іншими тут застосовні, звичайно. Найголовніше - інакше ви отримаєте помилкові результати при отриманні елементів із набору хеш-словника чи словника: хеш-код не повинен змінюватися залежно від часу життя об’єкта (точніше, протягом часу, коли потрібен хеш-код, наприклад, під час перебування ключ у словнику): наприклад, таке неправильне, оскільки Value є загальнодоступним, і тому його можна змінити зовнішньо до класу впродовж життя екземпляра, тому ви не повинні використовувати його як основу для хеш-коду:


   class A
   {
      public int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
      }
   }    

З іншого боку, якщо значення не можна змінити, це нормально:


   class A
   {
      public readonly int Value;

      public override int GetHashCode()
      {
         return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
      }
   }

3
Захищений. Це явно неправильно. Навіть Microsoft заявляє в MSDN ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ), що значення GetHashCode ОБОВ'ЯЗКОВО змінюватись, коли стан об'єкта змінюється так, що може вплинути на значення повернення виклику до рівня (), і навіть у своїх прикладах він також показує реалізацію GetHashCode, які повністю залежать від загальнодоступних значень.
— Себастьян PR Гінгтер

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

2
Себастьян, Крім того, я не бачу твердження у посиланні ( msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ), що GetHashCode () повинен змінити. Навпаки - він НЕ повинен змінюватися до тих пір, поки Equals повертає одне і те ж значення для того ж аргументу: "Метод GetHashCode для об'єкта повинен послідовно повертати один і той же хеш-код, доки не буде зміна стану об'єкта, що визначає повернене значення" методу рівняння об'єкта "." Це твердження не означає, що воно повинно змінюватися, якщо значення повернення для рівняння змінюється.
— ILoveFortran

2
@Joao, ви плутаєте сторону клієнта / споживача договору з виробником / виконавцем. Я кажу про відповідальність виконавця, який перекриває GetHashCode (). Ви говорите про споживача, того, хто використовує цінність.
— ILoveFortran

1
Повне непорозуміння ... :) Правда полягає в тому, що хеш-код повинен змінюватися, коли змінюється стан об'єкта, якщо стан не має значення для ідентичності об'єкта. Крім того, ви ніколи не повинні використовувати об'єкт MUTABLE як ключ у своїх колекціях. Використовуйте для цього об'єкти лише для читання. GetHashCode, Equals ... та деякі інші методи, імена яких я не пам’ятаю на даний момент, НІКОЛИ не повинні кидати.
— darlove

0

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

Наприклад, припустимо, що ми зберігаємо деякі об’єкти у списку. Десь пізніше хтось фактично усвідомлює, що HashSet є набагато кращою альтернативою, наприклад, завдяки кращим пошуковим характеристикам. Це коли ми можемо потрапити в біду. Список буде внутрішньо використовувати порівняльник рівності за замовчуванням для типу, який означає рівний у вашому випадку, тоді як HashSet використовує GetHashCode (). Якщо вони поводяться по-різному, так і ваша програма. І майте на увазі, що подібні питання не найлегше вирішити.

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


0

Щодо .NET 4.7кращого способу переосмислення GetHashCode()показано нижче. Якщо ви орієнтовані на більш старі версії .NET, включіть нульовий пакет System.ValueTuple .

// C# 7.0+
public override int GetHashCode() => (FooId, FooName).GetHashCode();

За рівнем продуктивності цей метод перевершить більшість складених хеш-кодів. ValueTuple є structтак що не буде ніякого сміття, і основний алгоритм так швидко , як він отримує.


-1

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

ВЕДЕНО: Це було неправильно, оригінальний метод GetHashCode () не може забезпечити рівність двох значень. Хоча об'єкти, які є рівними, повертають один і той же хеш-код.


-6

Нижче використання рефлексії здається мені кращим варіантом, враховуючи загальнодоступні властивості, оскільки з цим вам не доведеться турбуватися про додавання / видалення властивостей (хоча це не такий поширений сценарій). Це я виявив і більш ефективним. (Порівняння часу за допомогою секундоміра Diagonistics).

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }

12
Очікується, що реалізація GetHashCode () буде дуже легкою. Я не впевнений, що використання відображення помітно в StopWatch на тисячах дзвінків, але це, безумовно, на мільйонах (подумайте про те, щоб викласти словник зі списку).
— bohdan_trotsenko
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.