Якщо хеш-код null завжди дорівнює нулю, у .NET


87

Враховуючи, що колекції люблять System.Collections.Generic.HashSet<>приймати nullяк набір членів, можна запитати, яким nullповинен бути хеш-код . Схоже, що фреймворк використовує 0:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

Це може бути (трохи) проблематично з онульованими переліками. Якщо визначимо

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

тоді Nullable<Season>(також називається Season?) може приймати лише п’ять значень, але два з них, а саме nullі Season.Spring, мають однаковий хеш-код.

Спокусливо написати "кращий" порівняльник рівності, такий:

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

Але чи є якась причина, чому nullповинен бути хеш-код 0?

РЕДАГУВАТИ / ДОДАТИ:

Деякі люди, здається, думають, що мова йде про перевизначення Object.GetHashCode(). Насправді це не так. (Однак автори .NET зробили заміну GetHashCode()в Nullable<>структурі, яка є релевантною.) Реалізація написаного користувачем параметра без параметрівGetHashCode() ніколи не може впоратись із ситуацією, коли знаходиться об’єкт, чий хеш-код ми шукаємо null.

Мова йде про реалізацію абстрактного методу EqualityComparer<T>.GetHashCode(T)або про інший спосіб реалізації методу інтерфейсу IEqualityComparer<T>.GetHashCode(T). Тепер, створюючи ці посилання на MSDN, я бачу, що там сказано, що ці методи кидають ArgumentNullExceptionif, якщо їх єдиним аргументом є null. Це, звичайно, помилка на MSDN? Жодна з власних реалізацій .NET не створює винятків. Кидання в такому випадку ефективно розірве будь-яку спробу додати nullдо HashSet<>. Хіба що HashSet<>не робить щось надзвичайне при роботі з nullпредметом (мені доведеться це перевірити).

НОВИЙ РЕДАКТ / ДОДАТОК:

Тепер я спробував налагодити. З HashSet<>, я можу підтвердити , що з компаратором за замовчуванням рівності, значення Season.Springі null буде кінець у тому ж відрі. Це можна визначити, дуже ретельно перевіривши приватні члени масиву m_bucketsта m_slots. Зверніть увагу, що індекси завжди за своїм дизайном компенсуються одиницею.

Однак код, який я наводив вище, цього не виправляє. Як виявляється, HashSet<>ніколи навіть не запитають про порівняння рівності, коли значення null. Це з вихідного коду HashSet<>:

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

Це означає, що, принаймні для HashSet<>, навіть неможливо змінити хеш null. Натомість рішенням є зміна хешу всіх інших значень, наприклад:

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}

1
По-друге, це дуже гарне запитання.
Sachin Kainth

26
Чому хеш-код для null не повинен бути нульовим? Ви знаєте, хеш-зіткнення - це не кінець світу.
Hot Licks

3
За винятком того, що це добре відоме, досить поширене зіткнення. Не те, що це погано або навіть така велика проблема, її просто легко уникнути
Кріс Пфоль,

8
ха-ха, чому я думаю, "якщо фреймворк .NET зіскочить з мосту, ви підете за ним?" ...
Адам Голдсворт,

3
Просто з цікавості, яким би був нульовий сезон?
SwDevMan81,

Відповіді:


25

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

Повернення 0 або -1 за нуль, якщо ви вибрали один і постійно повертали його, буде працювати. Очевидно, що ненульові хеш-коди не повинні повертати будь-яке значення, яке ви використовуєте для null.

Схожі питання:

GetHashCode на нульових полях?

Що повинен повертати GetHashCode, коли ідентифікатор об'єкта нульовий?

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

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

Цікава знахідка, до речі.

Ще однією проблемою, яку я бачу в цьому, є те, що хеш-код не може представляти 4-байтовий або більший тип, який може бути онульований без принаймні одного зіткнення (більше із збільшенням розміру типу). Наприклад, хеш-код int - це просто int, тому він використовує повний діапазон int. Яке значення в цьому діапазоні ви вибрали для нуля? Що б ви не вибрали, зіткнеться з самим хеш-кодом значення.

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


Я не думаю, що запитання, на які ви посилаєтесь, абсолютно схожі. Коли ви перевизначаєте Object.GetHashCode()свій власний клас (або структуру), ви знаєте, що цей код буде вражений лише тоді, коли люди насправді мають екземпляр вашого класу. Цього екземпляру бути не може null. Ось чому ви не починаєте своє скасування Object.GetHashCode()з. if (this == null) return -1;Існує різниця між "бути null" і "бути об'єктом, що володіє деякими полями, які є null".
Jeppe Stig Nielsen

Ви кажете: Очевидно, що ненульові хеш-коди не повинні повертати будь-яке значення, яке ви використовуєте для null. Це було б ідеально, я згоден. І це причина, чому я в першу чергу задав своє запитання, тому що, коли ми пишемо перерахування T, тоді (T?)nullі (T?)default(T)буде мати однаковий хеш-код (у поточній реалізації .NET). Це можна було б змінити, якби розробники .NET змінили або хеш-код, null або алгоритм хеш-коду System.Enum.
Jeppe Stig Nielsen

Я згоден, що посилання були для нульових внутрішніх полів. Ви згадуєте, що це для IEqualityComparer <T>, у вашій реалізації хеш-код все ще є специфічним для типу, тому ви все ще перебуваєте в тій же ситуації, що відповідає типу. Повернення того самого хеш-коду для нулів будь-якого типу не матиме значення, оскільки нулі не мають типу.
Адам Голдсворт,

1
Примітка: Я двічі оновлював своє запитання. Виявляється, (принаймні з HashSet<>) не вдається змінити хеш-код null.
Jeppe Stig Nielsen

6

Майте на увазі, що хеш-код використовується як перший крок у визначенні лише рівності, і [використовується / не повинен] ніколи (не) використовується як фактичне визначення щодо рівності двох об’єктів.

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

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

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

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


5

Це не повинно бути нулем - ви можете зробити це 42, якщо хочете.

Важливо лише послідовність під час виконання програми.

Це просто найочевидніше подання, оскільки nullвоно часто представляється нулем всередині. Це означає, що під час налагодження, якщо ви бачите хеш-код нульовий, це може запропонувати вам подумати: "Хм ... це була нульова проблема з посиланнями?"

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


4

Хороше питання.

Я просто спробував закодувати це:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

і виконайте це так:

Season? v = null;
Console.WriteLine(v);

воно повертається null

якщо я це роблю, натомість нормально

Season? v = Season.Spring;
Console.WriteLine((int)v);

повернеться 0, як очікувалося, або проста Весна, якщо ми уникаємо кастингу int.

Отже .. якщо ви зробите наступне:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

РЕДАГУВАТИ

З MSDN

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

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

Знову з MSDN:

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


6
зіткнення, за визначенням, означає, що два нерівні об'єкти мають однаковий хеш-код. Ви продемонстрували, що об’єкти не рівні. Тепер у них однаковий хеш-код? Відповідно до ОП, яке вони роблять, означає, що це зіткнення. Зараз зіткнення не є кінцем світу, це просто більш вірогідне зіткнення, ніж якщо значення null хешується до чогось іншого, ніж 0, що шкодить продуктивності.
Серві

1
То що насправді говорить ваша відповідь? Ви говорите, що Season.Spring не дорівнює нулю. Ну, це не неправильно, але це насправді ніяк не відповідає на питання, зараз це робить.
Серві

2
@Servy: питання говорить: чому я маю однаковий hascode для 2 різних об'єктів ( null та Spring ). Отже, відповідь полягає в тому, що не існує причин зіткнення, навіть маючи однаковий хеш-код, вони, до речі, не рівні.
Тігран

3
"Відповідь: чому ні?" Ну, ОП попередньо відповів на ваше запитання "чому б і ні". Це швидше за все спричинить зіткнення, ніж інше число. Він цікавився, чи була причина обрана 0, і досі ніхто на це не відповів.
Серві

1
Ця відповідь не містить нічого, чого ОП ще не знає, що видно з того, як було задано питання.
Конрад Рудольф

4

Але чи є якась причина, чому хеш-код null повинен бути 0?

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

Хеш-функція абсолютно повинна повертати той самий хеш для того самого значення. Після того, як існує в компонент , який робить це, це дійсно єдине допустиме значення для хеш null. Якби для цього існувала константа, наприклад, hm object.HashOfNull, тоді хтось, хто реалізує a, IEqualityComparerповинен був би знати, щоб використовувати це значення. Якщо вони не замислюються над цим, я думаю, шанс, що вони використають 0, трохи вищий за будь-яке інше значення.

принаймні для HashSet <> навіть неможливо змінити хеш null

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


Коли хтось реалізує метод EqualityComparer<T>.GetHashCode(T)для певного типу, Tщо дозволяє null, потрібно щось робити, коли аргумент є null. Ви можете (1) кинути ArgumentNullException, (2) повернути 0або (3) повернути щось інше. Я приймаю вашу відповідь за рекомендацію завжди повертатися 0в такій ситуації?
Jeppe Stig Nielsen

@JeppeStigNielsen Я не впевнений щодо кидка проти повернення, але якщо ви все-таки вирішите повернутися, то точно нуль.
Роман Старков

2

Для простоти воно дорівнює 0. Не існує такої жорсткої вимоги. Потрібно лише забезпечити загальні вимоги хеш-кодування.

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

Звичайно, я обмежив свою відповідь вимогами математичного характеру. Існують також специфічні .NET, технічні умови, які ви можете прочитати тут . 0 для нульового значення серед них немає.


1

Отже, цього можна було б уникнути, використовуючи Unknownзначення переліку (хоча це здається трохи дивним, якщо a Seasonневідомий). Отже, щось подібне заперечує цю проблему:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

Тоді ви отримаєте унікальні значення хеш-коду для кожного сезону.


1
так, але це насправді не відповідає на запитання. Таким чином, відповідно до запитання, null буде співпадати з Uknown. Що таке різниця?
Тігран

@Tigran - Ця версія не використовує типовий тип
SwDevMan81

Я розумію, але питання стосується типу, що має дозвіл.
Тігран

У мене є мільйон разів на тему SO, яку люди пропонують для покращення як відповіді.
SwDevMan81,

1

Особисто я вважаю використання анульованих значень трохи незручним і намагаюся уникати їх, коли тільки можу. Ваша проблема - лише чергова причина. Іноді вони дуже зручні, але моє ескізне правило - не змішувати типи значень з null, якщо це можливо, просто тому, що вони з двох різних світів. У .NET framework вони, схоже, роблять те саме - багато типів значень забезпечують TryParseметод, який є способом відокремлення значень від жодного значення (null ).

У вашому конкретному випадку легко позбутися проблеми, оскільки ви обробляєте свій власний Seasonтип.

(Season?)nullдля мене означає "сезон не вказаний", як коли у вас є веб-форма, де деякі поля не є обов'язковими. На мій погляд, краще вказати це спеціальне "значення" enumсаме по собі, а не використовувати трохи незграбне Nullable<T>. Це буде швидше (без боксу) легше читати ( Season.NotSpecifiedпроти null) і вирішить вашу проблему з хеш-кодами.

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


Коли ви говорите "бокс", я думаю, ви маєте на увазі "обгортання", тобто введення значення структури всередину Nullable<>структури (де для HasValueчлена буде встановлено значення true). Ви впевнені, що проблема насправді менша int?? Багато часу один використовує лише кілька значень int, і тоді це еквівалентно перерахуванню (яке теоретично може мати багато членів).
Jeppe Stig Nielsen

Як правило, я б сказав, що перерахування вибирається тоді, коли потрібна обмежена кількість відомих значень (2-10). Якщо ліміт більший або його немає, це intмає більше сенсу. Звичайно, уподобання різняться.
Мацей

0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2

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