在.NET中,如果null的哈希码始终为零

时间:2012-05-23 15:43:33

标签: c# .net hash null

鉴于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?)只能获取五个值,但其中两个,即nullSeason.Spring具有相同的哈希码。< / p>

写这样一个“更好”的平等比较器是很诱人的:

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是否有任何理由?

修改/ ADDITION:

有些人似乎认为这是重写Object.GetHashCode()。实际上,它确实不是。 (.NET的作者确实在GetHashCode()结构中覆盖了Nullable<> 相关的结构。)用户编写的无参数{{1}实现我们永远无法处理我们寻找的哈希码为GetHashCode()的对象的情况。

这是关于实现抽象方法EqualityComparer<T>.GetHashCode(T)或以其他方式实现接口方法IEqualityComparer<T>.GetHashCode(T)。现在,在创建这些指向MSDN的链接时,我看到它说如果这些方法的唯一参数是null,则会抛出ArgumentNullException。这肯定是MSDN上的一个错误? .NET自己的实现都没有抛出异常。在这种情况下投掷将有效地破坏任何将null添加到null的尝试。除非HashSet<>在处理HashSet<>项目时做了一些特别的事情(我将不得不对此进行测试)。

新编辑/添加:

现在我尝试了调试。使用null,我可以确认使用默认的相等比较器,值HashSet<>Season.Spring 将在同一个存储桶中结束。这可以通过非常仔细地检查私有阵列成员nullm_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

9 个答案:

答案 0 :(得分:23)

只要为类型返回的空值的哈希码是一致的,你应该没问题。对哈希码的唯一要求是两个被认为相等的对象共享相同的哈希码。

返回0或-1表示null,只要您选择一个并且一直返回它,就可以了。显然,非空哈希码不应返回用于null的任何值。

类似问题:

GetHashCode on null fields?

What should GetHashCode return when object's identifier is null?

MSDN entry的“备注”详细介绍了哈希码。令人遗憾的是,该文档并未提供任何所有的空值的讨论或讨论 - 甚至在社区内容中也没有。

要解决enum的问题,要么重新实现哈希码以返回非零值,请添加一个等效于null的默认“unknown”枚举条目,或者只是不使用可为空的枚举。

顺便说一下,有趣的发现。

我看到的另一个问题通常是哈希码不能表示可以为空的4字节或更大的类型,而 至少有一次冲突 (更多类型大小增加)。例如,int的哈希码只是int,因此它使用完整的int范围。您为该范围选择什么值为null?无论你选择哪一个都会与值的哈希码本身发生碰撞。

碰撞本身并不一定是个问题,但你需要知道它们在那里。哈希码仅在某些情况下使用。正如MSDN上的文档所述,哈希代码不能保证为不同的对象返回不同的值,因此不应该这样。

答案 1 :(得分:6)

请记住,哈希码仅用作确定相等性的第一步,[is / should]永远不会被用作关于两个对象是否相等的事实上的确定。

如果两个对象的哈希码不相等,那么它们被视为不相等(因为我们假设无效的实现是正确的 - 即我们不会猜测那个)。如果它们具有相同的哈希码,则应检查它们是否为实际相等,在您的情况下,null和枚举值将失败。

结果 - 使用零与一般情况下的任何其他值一样好。

当然,有一些情况,比如你的枚举,这个零与真实的值的哈希码共享。问题是,对于你来说,额外比较的微不足道的开销是否会导致问题。

如果是这样,那么为你的特定类型的nullable的情况定义你自己的比较器,并确保null值总是产生一个总是相同的哈希码(当然!)一个不能由底层类型自己的哈希码算法产生的值。对于您自己的类型,这是可行的。对于其他人 - 祝你好运:)

答案 2 :(得分:5)

为零 - 如果您愿意,可以将其设为42。

在执行程序期间,重要的是一致性

这只是最明显的表示,因为null通常在内部表示为零。这意味着,在调试时,如果您看到哈希码为零,则可能会提示您思考,“嗯..这是一个空引用问题吗?”

请注意,如果你使用像0xDEADBEEF之类的数字,那么有人可能会说你正在使用一个神奇的数字......而你会有所帮助。 (你可以说零也是一个神奇的数字,你会说得对......除了它被广泛使用以至于对规则有点例外。)

答案 3 :(得分:4)

好问题。

我只是想对此进行编码:

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

并执行此操作:

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

返回null

如果我这样做,而不是正常的

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

如果我们避免转换为0,它会按预期返回int或简单 Spring

所以......如果你这样做:

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

编辑

来自MSDN

如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值。 但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值

换句话说:如果两个对象具有相同的哈希码并不意味着它们相等,则实际相等由 Equals 决定。

再次从MSDN:

  

对象的GetHashCode方法必须始终返回相同的值   哈希码只要没有对象状态的修改即可   确定对象的Equals方法的返回值。注意   这仅适用于当前执行的应用程序,并且   如果运行应用程序,则可以返回不同的哈希代码   试。

答案 4 :(得分:4)

  

但是有没有理由为什么null的哈希码应该是0?

它本可以是任何东西。我倾向于同意0不一定是最好的选择,但它可能导致最少的错误。

绝对的哈希函数必须为相同的值返回相同的哈希值。一旦存在执行此操作的 组件,这实际上是null哈希值的唯一有效值。如果有一个常量,例如,hm,object.HashOfNull,那么实现IEqualityComparer的人必须知道使用该值。如果他们没有考虑,那么他们使用0的机会略高于其他每一个值,我估计。

  

至少对于HashSet&lt;&gt;,甚至无法更改null的散列

如上所述,我认为完全停止是完全不可能的,因为存在类型已经遵循惯例,即null的哈希为0。

答案 5 :(得分:2)

为简单起见,它为0。没有这样的硬性要求。您只需要确保哈希编码的一般要求。

例如,您需要确保如果两个对象相等,则它们的哈希码必须始终相等。因此,不同的哈希码必须始终代表不同的对象(但不一定是真的反过来:两个不同的对象可能具有相同的哈希码,即使这经常发生,那么这不是一个好的哈希函数 - 它没有良好的抗碰撞性。)

当然,我限制了对数学本质要求的回答。还有特定于.NET的技术条件,您可以阅读here。 0表示空值不在其中。

答案 6 :(得分:1)

因此可以通过使用Unknown枚举值来避免这种情况(尽管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

然后,每个季节都会有唯一的哈希码值。

答案 7 :(得分:1)

就我个人而言,我发现使用可空值有点尴尬,并尽量避免使用它们。你的问题只是另一个原因。有时它们非常方便,但我的经验法则不是将值类型与null混合,如果可能的话,因为它们来自两个不同的世界。在.NET框架中,它们似乎也是如此 - 许多值类型提供了TryParse方法,这种方法可以将值与无值(null)分开。

在您的特定情况下,您可以轻松解决问题,因为您处理自己的Season类型。

(Season?)null对我来说意味着“没有指定季节”,就像你有一个不需要某些字段的webform一样。在我看来,最好在enum本身中指定特殊的“值”,而不是使用一点笨重的Nullable<T>。它会更快(没有拳击)更容易阅读(Season.NotSpecified vs null),并将解决您的哈希码问题。

当然对于其他类型,例如int,您无法扩展值域并将其中一个值命名为 special 并非总是可行。但是使用int?哈希代码碰撞是一个小得多的问题,如果有的话。

答案 8 :(得分:0)

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