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

Учитывая, что коллекции нравятся 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?

EDIT / Сложение:

Некоторые люди, кажется, думают, что это о переопределении 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;
  }
}

8 ответов

Решение

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

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

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

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

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

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

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

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

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

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

Это не должно быть ноль - вы можете сделать это 42, если хотите.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 объекта. Обратите внимание, что это верно только для текущего выполнения приложения, и что другой хэш-код может быть возвращен, если приложение будет запущено снова.

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

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

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

Так что этого можно избежать с помощью Unknown значение enum (хотя это кажется немного странным для 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

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

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

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

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

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

Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Другие вопросы по тегам