Чому важливо переосмислити 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.