Если нулевой хэш-код всегда равен нулю, в .NET

87

Учитывая, что такие коллекции, как System.Collections.Generic.HashSet<>accept 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

Это может быть (немного) проблематично с перечислениями, допускающими значение NULL. Если мы определим

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, я вижу, что там сказано, что эти методы генерируют, ArgumentNullExceptionесли их единственный аргумент - 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
lol, почему я думаю: «Если .NET framework спрыгнет с моста, вы последуете за ним?»
Адам Хоулдсворт,
3
Просто из любопытства, каким будет нулевой сезон?
SwDevMan81

Ответы:

25

Если хэш-код, возвращаемый для значений NULL, соответствует типу, все будет в порядке. Единственное требование для хэш-кода - это то, что два объекта, которые считаются равными, имеют один и тот же хэш-код.

Возврат 0 или -1 для null, если вы выбираете один и все время возвращаете его, будет работать. Очевидно, что ненулевые хэш-коды не должны возвращать любое значение, которое вы используете для null.

Похожие вопросы:

GetHashCode для пустых полей?

Что должен вернуть GetHashCode, если идентификатор объекта равен нулю?

«Примечания» этой записи MSDN более подробно относятся к хэш-коду. Любопытно отметить , что документация не дает никакого освещения или обсуждение значений нуля на всех - даже не в содержании сообщества.

Чтобы решить проблему с перечислением, либо повторно реализуйте хэш-код, чтобы он возвращал ненулевое значение, добавьте запись перечисления по умолчанию «unknown», эквивалентную null, либо просто не используйте перечисления, допускающие значение NULL.

Кстати, интересная находка.

Другая проблема, с которой я обычно сталкиваюсь, заключается в том, что хеш-код не может представлять тип размером 4 байта или более, допускающий значение NULL, без хотя бы одного столкновения (больше по мере увеличения размера типа). Например, хэш-код int - это просто int, поэтому он использует полный диапазон int. Какое значение в этом диапазоне вы выбираете для null? Какой бы из них вы ни выбрали, он столкнется с самим хеш-кодом значения.

Столкновения сами по себе не обязательно являются проблемой, но вы должны знать, что они есть. Хеш-коды используются только в некоторых случаях. Как указано в документации MSDN, хэш-коды не гарантируют возврат разных значений для разных объектов, поэтому ожидать этого не следует.

Адам Хулдсворт
источник
Я не думаю, что вопросы, на которые вы ссылаетесь, полностью похожи. Когда вы переопределяете Object.GetHashCode()в своем собственном классе (или структуре), вы знаете, что этот код будет задействован только тогда, когда у людей действительно есть экземпляр вашего класса. Такого экземпляра быть не может null. Вот почему вы не начинаете переопределение Object.GetHashCode()с. if (this == null) return -1;Существует разница между «быть null» и «быть объектом, обладающим некоторыми полями, которые есть null».
Йеппе Стиг Нильсен,
Вы говорите: очевидно, что ненулевые хэш-коды не должны возвращать любое значение, которое вы используете для null. Я согласен, это было бы идеально. И именно по этой причине я задал свой вопрос в первую очередь, потому что всякий раз, когда мы пишем перечисление T, тогда (T?)nullи (T?)default(T)будет иметь один и тот же хэш-код (в текущей реализации .NET). Это можно было бы изменить, если бы разработчики .NET изменили либо хэш-код, null либо алгоритм хэш-кода платформы System.Enum.
Йеппе Стиг Нильсен
Я согласен, что ссылки были для пустых внутренних полей. Вы упоминаете, что это для IEqualityComparer <T>, в вашей реализации хэш-код по-прежнему специфичен для типа, поэтому вы все еще находитесь в той же ситуации, согласованности для типа. Возврат одного и того же хэш-кода для значений NULL любого типа не имеет значения, поскольку значения NULL не имеют типа.
Адам Хулдсворт,
1
Примечание. Я обновил свой вопрос дважды. Получается, что (по крайней мере, с HashSet<>) поменять хеш-код null.
Йеппе Стиг Нильсен
6

Имейте в виду, что хэш-код используется только в качестве первого шага при определении равенства, и никогда (не должен) использоваться как фактическое определение того, равны ли два объекта.

Если хэш-коды двух объектов не равны, они рассматриваются как неравные (потому что мы предполагаем, что базовая реализация верна - т.е. мы не догадываемся об этом). Если у них один и тот же хэш-код, тогда они должны быть проверены на фактическое равенство, которое в вашем случае nullне удастся и значение перечисления.

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

Конечно, будут ситуации, такие как ваше перечисление, где этот ноль используется совместно с хеш-кодом реального значения. Вопрос в том, вызывает ли у вас незначительные накладные расходы на дополнительное сравнение.

Если это так, тогда определите свой собственный компаратор для случая обнуляемого для вашего конкретного типа и убедитесь, что нулевое значение всегда дает хэш-код, который всегда один и тот же (конечно!), И значение, которое не может быть получено базовым собственный алгоритм хэш-кода типа. Для ваших собственных типов это выполнимо. Для остальных - удачи :)

Андраш Золтан
источник
5

Оно не обязательно должно быть нулевым - вы можете получить 42, если хотите.

Все, что имеет значение, - это последовательность во время выполнения программы.

Это просто наиболее очевидное представление, потому что nullвнутренне оно часто представляется как ноль. Это означает, что если во время отладки вы увидите нулевой хэш-код, это может побудить вас подумать: «Хм ... это проблема с нулевой ссылкой?»

Обратите внимание: если вы используете такое число 0xDEADBEEF, то кто-то может сказать, что вы используете магическое число ... и вы вроде бы так и сделали. (Вы могли бы сказать, что ноль - это тоже магическое число, и вы были бы правы ... за исключением того, что оно настолько широко используется, что является своего рода исключением из правила.)

пользователь541686
источник
4

Хороший вопрос.

Я просто попытался закодировать это:

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

и выполните это так:

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

он возвращается null

если я сделаю, вместо этого нормально

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

он вернет 0, как и ожидалось, или просто Spring, если мы избегаем приведения в int.

Итак .. если вы сделаете следующее:

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

РЕДАКТИРОВАТЬ

Из MSDN

Если два объекта сравниваются как равные, метод GetHashCode для каждого объекта должен возвращать одно и то же значение. Однако, если два объекта не сравниваются как равные, методы GetHashCode для двух объектов не должны возвращать разные значения.

Другими словами: если два объекта имеют одинаковый хэш-код, это не означает, что они равны, потому что реальное равенство определяется Equals .

Снова из MSDN:

Метод GetHashCode для объекта должен последовательно возвращать один и тот же хэш-код до тех пор, пока не будет изменено состояние объекта, определяющее возвращаемое значение метода Equals объекта. Обратите внимание, что это верно только для текущего выполнения приложения и что другой хэш-код может быть возвращен, если приложение запускается снова.

Тигран
источник
6
коллизия по определению означает, что два неравных объекта имеют один и тот же хэш-код. Вы показали, что объекты не равны. Теперь у них одинаковый хэш-код? Согласно OP они это делают, что означает, что это столкновение. Коллизия - не конец света, это просто более вероятная коллизия, чем если бы нулевой хеш-код имел значение, отличное от 0, что снижает производительность.
Servy
1
Так что же на самом деле говорит ваш ответ? Вы говорите, что Season.Spring не равно нулю. Что ж, в этом нет ничего плохого, но на самом деле он не отвечает на вопрос, а теперь это не так.
Обслуживание
2
@Servy: вопрос говорит: вот почему у меня одинаковый hascode для двух разных объектов ( null и Spring ). Итак, ответ заключается в том, что нет причины коллизии, даже имея один и тот же хэш-код, кстати, они не равны.
Тигран
3
"Ответ: почему бы и нет?" Что ж, ОП заранее ответил на ваш вопрос «почему бы и нет». Это более вероятно вызовет столкновения, чем другое число. Ему было интересно, была ли причина, по которой был выбран 0, но пока никто не ответил на это.
Servy
1
Этот ответ не содержит ничего, о чем OP еще не знает, что очевидно из того, как был задан вопрос.
Конрад Рудольф
4

Но есть ли причина, по которой хэш-код null должен быть 0?

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

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

по крайней мере, для HashSet <> невозможно даже изменить хэш нуля

Как упоминалось выше, я думаю, что полная остановка невозможна просто потому, что существуют типы, которые уже следуют соглашению о том, что хэш нулевого значения равен 0.

Роман Старков
источник
Когда кто-то реализует метод EqualityComparer<T>.GetHashCode(T)для некоторого конкретного типа, Tкоторый позволяет null, он должен что- то делать, когда аргумент есть null. Вы можете (1) бросить ArgumentNullException, (2) вернуть 0или (3) вернуть что-то еще. Я принимаю ваш ответ за рекомендацию всегда возвращаться 0в такой ситуации?
Jeppe Stig Nielsen
@JeppeStigNielsen Я не уверен насчет throw vs return, но если вы решите вернуться, то определенно ноль.
Роман Старков
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

Тогда у вас будут уникальные значения хэш-кода для каждого сезона.

SwDevMan81
источник
1
да, но на самом деле это не ответ на вопрос. Таким образом, согласно вопросу null будет конфликтовать с Uknown. В чем разница?
Тигран
@Tigran - В этой версии не используется тип, допускающий значение NULL
SwDevMan81
Понятно, но вопрос в типе, допускающем значение NULL.
Тигран
У меня миллион раз сцены SO, которые люди предлагают в качестве ответов для улучшения.
SwDevMan81
1

Лично я нахожу использование значений, допускающих значение NULL, немного неудобным и стараюсь избегать их, когда могу. Ваша проблема - это еще одна причина. Иногда они очень удобны, но мое практическое правило - не смешивать типы значений с null, если это возможно, просто потому, что они из двух разных миров. В платформе .NET они, похоже, делают то же самое - многие типы значений предоставляют TryParseметод, который является способом отделения значений от значения без значения ( null).

В вашем конкретном случае от проблемы легко избавиться, потому что вы работаете со своим собственным Seasonтипом.

(Season?)nullДля меня это означает, что «сезон не указан», например, когда у вас есть веб-форма, в которой некоторые поля не обязательны. На мой взгляд, лучше указать это особое «значение» enumсамо по себе, чем использовать немного неуклюжее Nullable<T>. Это будет быстрее (без бокса), легче читать ( Season.NotSpecifiedvs null) и решит вашу проблему с хеш-кодами.

Конечно, для других типов, например, intвы не можете расширить область значений, и обозначить одно из значений как особенное не всегда возможно. Но с int?хэш-кодом проблема гораздо меньше, если вообще существует.

Мацей
источник
Когда вы говорите «бокс», я думаю, вы имеете в виду «упаковку», то есть размещение значения структуры внутри Nullable<>структуры (где HasValueзатем будет установлен член true). Вы уверены, что проблема действительно поменьше int?? Часто используется только несколько значений int, а затем это эквивалентно перечислению (которое теоретически может иметь много членов).
Йеппе Стиг Нильсен,
Обычно я бы сказал, что enum выбирается, когда требуется ограниченное количество известных значений (2-10). Если лимит больше или его нет, intимеет смысл. Конечно, предпочтения бывают разные.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Денис535
источник
1
Это интересный подход. Было бы полезно отредактировать свой ответ, включив в него некоторые дополнительные пояснения, особенно с учетом характера вопроса.
Джереми Кейни,