Реалізація за замовчуванням для Object.GetHashCode ()


162

Як працює реалізація за замовчуванням для GetHashCode()роботи? І чи ефективно обробляє структури, класи, масиви тощо?

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


Подивіться коментар, який я залишив до статті: stackoverflow.com/questions/763731/gethashcode-extension-method
Пол Весткотт,


34
Убік: ви можете отримати хеш-код за замовчуванням (навіть коли GetHashCode()це було відмінено), скориставшисьSystem.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell дякую вам за це, я шукав саме цю відповідь.
Андрій Савіних

@MarcGravell Але як би це зробити за допомогою іншого методу?
Томаш Зато - Відновіть Моніку

Відповіді:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCode відображається на функцію ObjectNative :: GetHashCode в CLR, яка виглядає приблизно так:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

Повна реалізація GetHashCodeEx досить велика, тому простіше просто посилання на вихідний код C ++ .


5
Ця цитата документації повинна виходити з дуже ранньої версії. Він більше не пишеться так у нинішніх статтях MSDN, напевно, тому що це зовсім неправильно.
Ганс Пасант

4
Вони змінили формулювання, так, але воно все ще говорить в основному те саме: "Отже, реалізація цього методу за замовчуванням не повинна використовуватися як унікальний ідентифікатор об'єкта для цілей хешування".
Девід Браун

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

3
@ ta.speot.is: Якщо ви хочете визначити, чи певний екземпляр уже додано до словника, рівність посилань ідеальна. Як зазначаєте, рядки, як правило, більше цікавлять, чи вже додано рядок, що містить ту саму послідовність символів . Ось чому stringвідміняється GetHashCode. З іншого боку, припустимо, ви хочете вести підрахунок, скільки разів різні елементи керування обробляють Paintподії. Ви можете використати Dictionary<Object, int[]>(кожен int[]зберігається міститиме рівно один предмет).
supercat

6
@ ItNotALie. Тоді подякуйте Archive.org за те, що він отримав копію ;-)
RobIII

88

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

При переосмисленні рівності у вас завжди має бути відповідність Equals()і GetHashCode()(тобто для двох значень, якщо Equals()повертає true, вони повинні повернути один і той же хеш-код, але зворотне не потрібно) - і звичайно також надавати ==/ !=операторів, а часто і реалізувати IEquatable<T>теж.

Для генерації хеш-коду звичайно використовувати фактичну суму, оскільки це дозволяє уникнути зіткнень щодо парних значень - наприклад, для основного хеша 2 поля:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

Це має ту перевагу, що:

  • хеш {1,2} не такий, як хеш {2,1}
  • хеш {1,1} не такий, як хеш {2,2}

тощо - що може бути загальним, якщо ви просто використовуєте незважену суму або xor ( ^) тощо.


Відмінна думка про користь алгоритму з врахованою сумою; чогось я раніше не усвідомлював!
Лазівка

Чи не призведуть фактичні суми (як написано вище) періодично?
sinelaw

4
@sinelaw так, це слід виконати unchecked. На щастя, uncheckedце за замовчуванням у C #, але краще було б зробити це явним; відредаговано
Marc Gravell

7

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

Основні типи даних , такі як byte, short, int, long, charі stringреалізувати метод добре GetHashCode. Деякі інші класи та структури, як, Pointнаприклад, реалізують GetHashCodeметод, який може бути або не підходить для ваших конкретних потреб. Ви просто повинні спробувати це, щоб зрозуміти, чи достатньо це добре.

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


2
Я не погоджуюся, що вам слід регулярно додавати власну реалізацію. Просто переважна більшість класів (зокрема) ніколи не перевірятиметься на рівність - або там, де вони є, вбудована опорна рівність є чудовою. У (вже рідкісному) випадку написання структури це було б більш поширеним, правдивим.
Марк Гравелл

@Marc Gravel: Це, звичайно, не те, що я мав на увазі. Я відкоригую останній абзац. :)
Guffa

Основні типи даних не реалізують хороший метод GetHashCode, принаймні в моєму випадку. Наприклад, GetHashCode for int повертає саме число: (123) .GetHashCode () повертає 123.
fdermishin

5
@ user502144 І що з цим погано? Це ідеальний унікальний ідентифікатор, який легко обчислити, без помилкових позитивних результатів щодо рівності ...
Річард Раст

@Richard Rast: Це нормально, за винятком того, що ключі можуть бути погано розподілені при використанні в Hashtable. Погляньте на цей відповідь: stackoverflow.com/a/1388329/502144
fdermishin

5

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


1

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

Рівний використовується при перевірці Foo A, B;

якщо (A == B)

Оскільки ми знаємо, що вказівник не відповідає, ми можемо порівняти внутрішні члени.

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

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

Я, як правило,

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

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

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


2
Підхід ^ = не є особливо хорошим підходом для створення хешу - він, як правило, призводить до безлічі спільних / передбачуваних зіткнень - наприклад, якщо Prop1 = Prop2 = 3.
Marc Gravell

Якщо значення однакові, я не бачу проблеми зіткненням, оскільки об'єкти рівні. 13 * Hash + NewHash здається цікавим, хоча.
Беннетт Кріп

2
Бен: спробуйте для Obj1 {Prop1 = 12, Prop2 = 12} і Obj2 {Prop1 = 13, Prop2 = 13}
Томаш Кафка

0

Якщо ви просто маєте справу з POCO, ви можете використовувати цю утиліту, щоб дещо спростити своє життя:

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

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