在.NET中,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 ,具有相同的哈希码。

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

 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的作者确实在Nullable<>结构中重写了GetHashCode() ,但是它相关的)。用户编写的无参数GetHashCode()实现永远无法处理哈希码对象寻求为null

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

新的编辑/添加:

现在我尝试debugging。 使用HashSet<> ,我可以确认,使用默认的相等比较器,值Season.Springnull 在同一个桶中结束。 这可以通过非常仔细地检查私有数组成员m_bucketsm_slots来确定。 请注意,索引总是在devise上偏移1。

我上面给出的代码并不能解决这个问题。 事实certificate,当值为null时, HashSet<>将永远不会询问相等比较器。 这是来自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; } 

这意味着至less对于HashSet<> ,甚至不可能改变null的散列null 相反,解决scheme是更改所有其他值的散列,如下所示:

 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; } } 

只要为空值返回的散列码与该types一致 ,就应该没问题。 哈希码的唯一要求是两个被视为相同的对象共享相同的哈希码。

返回0或-1为null,只要你select一个并且一直返回,就会工作。 很明显,非null散列码不应该返回你用于null的任何值。

类似的问题:

GetHashCode在空字段?

当对象的标识符为空时,GetHashCode应该返回什么?

这个MSDN条目的“备注”在哈希码周围有更多的细节。 有意思的是,文档没有提供关于空值的任何报道或讨论 – 甚至在社区内容中也没有。

为了解决你的问题与枚举,要么重新实现哈希代码返回非零,添加一个默认的“未知”枚举项等效于null,或者根本不使用可为空的枚举。

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

我通常会看到的另一个问题是,散列码不能代表一个4字节或更大的types,如果没有至less一次冲突 (更多types的大小增加),它可以为空。 例如,int的哈希码就是int,所以它使用完整的int范围。 你selectnull的那个值是多less? 无论你select哪一个,都将与该值的散列码本身发生冲突。

本身的碰撞不一定是个问题,但你需要知道他们在那里。 散列码仅在某些情况下使用。 正如在MSDN文档中所述,散列码不能保证为不同的对象返回不同的值,所以不应该被期望。

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

如果两个对象的哈希码不相等,那么它们被认为是不相等的(因为我们假设这个可执行的实现是正确的 – 也就是说我们不会再次猜测)。 如果他们有相同的哈希码,那么他们应该检查实际的平等,在你的情况下, null和枚举值将失败。

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

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

如果是这样的话,那么为你的特定types的可为空的情况定义你自己的比较器,并确保一个空值总是产生一个总是相同的哈希代码(当然!) 一个不能被底层types自己的散列码algorithm。 对于你自己的types,这是可行的。 对于其他人 – 祝你好运:)

它不一定是零 – 如果你愿意,你可以把它做成42。

重要的是在程序执行过程中的一致性

这只是最明显的表示,因为null在内部通常表示为零。 这意味着,在debugging的时候,如果你看到一个零的散列码,它可能会提示你想:“呃..这是一个空引用问题?

请注意,如果您使用像0xDEADBEEF这样的数字,那么有人可能会说您正在使用一个幻数…而且您会是这样。 (你可以说零也是一个神奇的数字,你会是对的…除了它是如此广泛的使用,是有点例外的规则。)

好问题。

我只是试图编码这个:

 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方法的返回值的对象状态。 请注意,这仅适用于应用程序的当前执行,并且如果应用程序再次运行,则可以返回不同的散列码。

但是为什么null的哈希码应该是0呢?

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

散列函数绝对必须为相同的值返回相同的散列值。 一旦存在这样做组件,这实际上是null的散列的唯一有效值。 如果有这样一个常量,比如hm, object.HashOfNull ,那么实现IEqualityComparer就必须知道使用该值。 如果他们不考虑这个问题,我估计他们使用0的机会比其他任何一个值都要高。

至less对于HashSet <>来说,甚至不可能改变null的散列值

如上所述,我认为这是完全不可能的,因为存在已经遵循约定的types,null的散列值为0。

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

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

当然,我限制了对math性质要求的回答。 有.NET特定的技术条件,你可以在这里阅读。 0为空值不在其中。

所以这可以通过使用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 

那么你将有每个季节独特的哈希码值。

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

在你的情况下,很容易摆脱这个问题,因为你处理自己的Seasontypes。

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

当然对于其他types,比如int你不能扩展价值领域并且把其中一个价值定为特殊的并不总是可能的。 但是用int? 哈希码碰撞是一个小得多的问题,如果有的话。