在.NET中应该为null的哈希码始终为零


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?)只能使用五个值,但是其中两个(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的作者并做出一个覆盖GetHashCode()Nullable<>其结构相关的,虽然)。无参数的用户编写的实现GetHashCode(),其中,其哈希码我们所追求的目标是永远不能处理的情况null

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

新编辑/添加:

现在我尝试调试。使用HashSet<>,我可以确认使用默认的相等比较器,这些值Season.Springnull 在同一存储桶中结束。这可以通过非常仔细地检查私有数组成员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
我第二个问题-非常好的问题。
萨钦·凯恩斯

26
为什么null的哈希码不为零?您知道,哈希冲突并不是世界末日。
Hot Licks 2012年

3
除了这是众所周知的,很常见的碰撞。不,这是不好的,甚至是主要的问题,它只是容易避免
克里斯Pfohl

8
大声笑我为什么想“如果.NET框架跳下桥,您会遵循吗?” ...
Adam Houldsworth,2012年

3
出于好奇,空季节将是什么?
SwDevMan81

Answers:


25

只要为空返回的哈希码与该类型一致,就可以了。哈希码的唯一要求是,被认为相等的两个对象共享相同的哈希码。

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

类似问题:

空字段上的GetHashCode吗?

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

MSDN条目的“备注”在哈希码周围有更多详细信息。尖锐,文档不提供任何覆盖或空值的讨论-甚至没有在社会上的内容。

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

有趣的是,找到了。

我通常会看到的另一个问题是哈希码无法表示4字节或更大的类型,该类型在没有至少一个冲突的情况下就可以为空(随着类型大小的增加,该类型也会更多)。例如,int的哈希码就是int,因此它使用完整的int范围。您选择该范围内的哪个值作为null?您选择的任何一种都会与值的哈希码本身发生冲突。

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


我认为您链接的问题并不完全相同。当您覆盖Object.GetHashCode()自己的类(或结构)时,您知道只有当人们实际拥有您的类的实例时,此代码才会被点击。该实例不能为null。这就是为什么你没有开始你的倍率Object.GetHashCode()if (this == null) return -1;有“存在的差异null”和“是具有某些字段是一个对象null”。
Jeppe Stig Nielsen'5

您说:显然,非null哈希码不应返回您用于null的任何值。我同意,那将是理想的。这就是为什么我首先问我的问题的原因,因为每当我们编写一个枚举时T,then(T?)null(T?)default(T)都将具有相同的哈希码(在当前的.NET实现中)。如果.NET的实现者更改的哈希码null 的哈希码算法,则可以更改System.Enum
Jeppe Stig Nielsen

我同意这些链接是针对空的内部字段。您提到它是针对IEqualityComparer <T>的,在您的实现中,哈希码仍特定于类型,因此您仍处在相同的情况下,即类型的一致性。对于任何类型的null返回相同的哈希码都没有关系,因为null没有类型。
亚当·霍兹沃思

1
注意:我两次更新了我的问题。事实证明,(至少使用HashSet<>)更改的哈希码无效null
杰普·斯蒂格·尼尔森

6

请记住,哈希码仅用作确定相等性的第一步,并且绝对不应用作确定两个对象是否相等的实际决定。

如果两个对象的哈希码不相等,则将它们视为不相等(因为我们假设不拘泥的实现是正确的-即我们不会对此进行第二次猜测)。如果它们具有相同的哈希码,则应检查它们的实际相等性,在您的情况下,null和枚举值将失败。

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

当然,在某些情况下(例如您的枚举),零与真实值的哈希码。问题是,对您来说,额外比较的微小开销是否会引起问题。

如果是这样,则为您的特定类型的nullable定义您自己的比较器,并确保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预期的结果,或者如果我们避免强制转换为,则返回简单的Springint

所以..如果您执行以下操作:

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

编辑

MSDN

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

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

再次从MSDN:

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


6
根据定义,冲突意味着两个不相等的对象具有相同的哈希码。您已证明对象不相等。现在它们是否具有相同的哈希码?根据OP的说法,这是一次碰撞。现在,发生冲突并不是世界末日,它比null散列为0以外的值更容易发生冲突,这会损害性能。
Servy

1
那么,您的答案实际上是怎么说的?您说Season.Spring不等于null。好吧,这没有错,但是现在它并没有以任何方式真正回答问题。
Servy

2
@Servy:问题说:为什么我对2个不同的对象(nullSpring)使用相同的hascode 。因此答案是,即使具有相同的哈希码也没有冲突原因,顺便说一句,它们不是相等的。
提格伦2012年

3
“答案:为什么不呢?” 好吧,OP抢先回答了您“为什么不”的问题。与其他数字相比,它更可能导致冲突。他想知道是否有选择0的原因,到目前为止,没有人回答。
Servy

1
从问题的询问方式可以明显看出,此答案不包含OP尚不知道的内容。
康拉德·鲁道夫2012年

4

但是,是否有任何原因为什么null的哈希码应为0?

可能什么都没有。我倾向于同意0不一定是最佳选择,但它可能导致最少的错误。

哈希函数绝对必须为相同的值返回相同的哈希。一旦存在执行此操作组件,这实际上是哈希值的唯一有效值null。如果有一个常数,例如hm object.HashOfNull,那么实现的某人IEqualityComparer将必须知道使用该值。我认为,如果他们不考虑这个问题,那么他们使用0的机会就会比其他所有值都高一些。

至少对于HashSet <>,甚至不可能更改null的哈希

如上所述,我认为这完全是不可能的,因为存在已经遵循null哈希为0的约定的类型。


EqualityComparer<T>.GetHashCode(T)为某种T允许的特定类型实现方法时,当参数为时null必须做一些事情null。您可以(1)抛出ArgumentNullException,(2)返回0,或(3)返回其他内容。0在这种情况下,我总是回答您的建议?
Jeppe Stig Nielsen 2012年

@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冲突。有什么区别?
提格伦2012年

@Tigran-此版本未使用可为null的类型
SwDevMan81'5

我知道了,但问题是关于可为空的类型。
提格伦2012年

我在SO上有一百万次,人们提出了改进建议作为答案。
SwDevMan81

1

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

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

(Season?)null对我来说意味着“未指定季节”,例如当您有一个不需要某些字段的网络表单时。我认为最好在enum而不要使用笨拙的方法Nullable<T>。它会更快(没有装箱),更易于阅读(Season.NotSpecifiedvs null),并且可以解决哈希码的问题。

当然,对于其他类型,例如int您不能扩展值域,并且不一定总是将其中一个值命名为特殊值。但是int?,如果有的话,与哈希码冲突将是一个较小的问题。


当您说“装箱”时,我认为您的意思是“包装”,即将结构值放入Nullable<>结构中(然后将HasValue成员设置为true)。您确定问题真的很小int?吗?很多时候,一个人只使用的几个值int,然后就相当于一个枚举(理论上可以有很多成员)。
杰普·斯蒂格·尼尔森

通常,我会说当所需的已知值数量有限(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

1
这是一个有趣的方法。编辑您的答案以包括一些其他解释会很有用,尤其是考虑到问题的性质。
杰里米·卡尼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.