为什么C#没有为集合实现GetHashCode?

时间:2010-05-25 18:27:32

标签: c# java collections hashcode gethashcode

我正在将一些东西从Java移植到C#。在Java中,hashcode的{​​{1}}取决于其中的项目。在C#中,我总是从ArrayList ...

获得相同的哈希码

这是为什么?

对于我的一些对象,哈希码需要不同,因为list属性中的对象使对象不相等。我希望哈希码对于对象的状态始终是唯一的,并且当对象相等时仅等于另一个哈希码。我错了吗?

7 个答案:

答案 0 :(得分:15)

为了正常工作,哈希码必须是不可变的 - 对象的哈希码必须永远不会更改。

如果对象的哈希码确实发生了变化,那么包含该对象的任何词典都将停止工作。

由于集合不是不可变的,因此无法实现GetHashCode 相反,它们继承了默认的GetHashCode,它为对象的每个实例返回(希望)唯一值。 (通常基于内存地址)

答案 1 :(得分:8)

是的,你错了。在Java和C#中,相等意味着具有相同的哈希码,但反过来并不(必然)为真。

有关详细信息,请参阅GetHashCode

答案 2 :(得分:8)

Hashcodes必须依赖于所使用的相等的定义,以便A == B然后A.GetHashCode() == B.GetHashCode()(但不一定是反向; A.GetHashCode() == B.GetHashCode()不需要A == B)。< / p>

默认情况下,值类型的相等定义基于其值,引用类型的相等定义基于它的标识(即,默认情况下,引用类型的实例仅等于其自身),因此值类型的默认哈希码使得它依赖于它包含的字段的值*,对于引用类型,它取决于标识。实际上,由于理想情况下我们希望非等对象的哈希码特别在低位(最有可能影响重新散列的值)的情况下不同,我们通常想要两个等价但是不相等的对象有不同的哈希值。

由于对象将保持与自身相等,因此即使对象发生变异,GetHashCode()的默认实现也将继续具有相同的值(即使对于可变对象,身份也不会发生变异)对象)。

现在,在某些情况下,引用类型(或值类型)重新定义相等性。一个例子是string,例如"ABC" == "AB" + "C"。虽然比较了两个不同的字符串实例,但它们被认为是相同的。在这种情况下,必须覆盖GetHashCode(),以便该值与定义相等的状态(在这种情况下,包含的字符序列)相关。

虽然使用也是不可变的类型更常见,但由于各种原因, GetHashCode()不依赖于不变性。相反,GetHashCode()必须在可变性面前保持一致 - 更改我们在确定哈希时使用的值,并且哈希必须相应地更改。但请注意,如果我们使用这个可变对象作为使用哈希的结构的键,这是一个问题,因为改变对象会改变它应该存储的位置,而不会将其移动到该位置(它&#39;对于集合中对象的位置取决于其值的任何其他情况也是如此 - 例如,如果我们对列表进行排序然后改变列表中的一个项目,则不再对列表进行排序)。但是,这并不意味着我们必须只在字典和散列集中使用不可变对象。相反,它意味着我们不能改变这种结构中的对象,并使其不可变是一种明确的方法来保证这一点。

实际上,有很多情况下需要在这种结构中存储可变对象,只要我们在这段时间内不变异,这就没问题了。由于我们不具备不变性所带来的保证,因此我们希望以另一种方式提供它(例如,在集合中花费很短的时间并且只能从一个线程访问)。

因此,关键值的不变性是可能出现问题的一种情况,但通常是一种想法。但是,对于定义哈希码算法的人来说,并不是他们认为任何这样的情况总是一个坏主意(他们甚至不知道在对象存储在这样的结构中时发生了突变) ;他们实现在对象的当前状态上定义的哈希码,无论是否在给定点调用它都是好的。因此,例如,除非在每个mutate上清除memoisation,否则不应在可变对象上记忆哈希码。 (无论如何,记忆哈希通常都是浪费,因为反复敲击相同对象哈希码的结构会有自己的备忘录。)

现在,在手头的情况下,ArrayList在基于身份的默认情况下进行操作,例如:

ArrayList a = new ArrayList();
ArrayList b = new ArrayList();
for(int i = 0; i != 10; ++i)
{
  a.Add(i);
  b.Add(i);
}
return a == b;//returns false

现在,这实际上是一件好事。为什么?那么,你怎么知道在上面我们要考虑a等于b?我们可能会,但在其他情况下也有很多充分的理由不这样做。

更重要的是,从基于身份到基于价值的重新定义平等要容易得多,而不是从基于价值的转变为基于身份的平等。最后,对于许多对象,有多个基于值的相等定义(经典案例是关于什么使字符串相等的不同视图),所以即使是一个唯一的定义也是如此。例如:

ArrayList c = new ArrayList();
for(short i = 0; i != 10; ++i)
{
  c.Add(i);
}

如果我们考虑上面的a == b,我们是否应该考虑a == c?答案取决于我们所使用的平等定义中我们关心的内容,因此框架无法知道所有案例的正确答案,因为所有案例都不同意。

现在,如果我们在特定情况下关注基于价值的平等,我们有两个非常简单的选择。第一个是子类化并超越平等:

public class ValueEqualList : ArrayList, IEquatable<ValueEqualList>
{
  /*.. most methods left out ..*/
  public Equals(ValueEqualList other)//optional but a good idea almost always when we redefine equality
  {
    if(other == null)
      return false;
    if(ReferenceEquals(this, other))//identity still entails equality, so this is a good shortcut
      return true;
    if(Count != other.Count)
      return false;
    for(int i = 0; i != Count; ++i)
      if(this[i] != other[i])
        return false;
    return true;
  }
  public override bool Equals(object other)
  {
    return Equals(other as ValueEqualList);
  }
  public override int GetHashCode()
  {
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

这假设我们总是希望以这种方式处理这样的列表。我们还可以为给定的案例实现IEqualityComparer:

public class ArrayListEqComp : IEqualityComparer<ArrayList>
{//we might also implement the non-generic IEqualityComparer, omitted for brevity
  public bool Equals(ArrayList x, ArrayList y)
  {
    if(ReferenceEquals(x, y))
      return true;
    if(x == null || y == null || x.Count != y.Count)
      return false;
    for(int i = 0; i != x.Count; ++i)
      if(x[i] != y[i])
        return false;
    return true;
  }
  public int GetHashCode(ArrayList obj)
  {
    int res = 0x2D2816FE;
    foreach(var item in obj)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

总结:

  1. 引用类型的默认相等定义仅取决于身份。
  2. 大多数时候,我们都想要那样。
  3. 当定义班级的人决定这不是想要什么时,他们可以改写这种行为。
  4. 当使用该类的人想要再次使用不同的相等定义时,他们可以使用IEqualityComparer<T>IEqualityComparer,因此他们的字典,散列图,哈希集等使用他们的相等概念。
  5. 当一个对象成为基于哈希的结构的关键时,它是一个灾难性的变异对象。可以使用不变性来确保这不会发生,但不是强制性的,也不是总是可取的。
  6. 总而言之,框架为我们提供了很好的默认值和详细的覆盖可能性。

    *结构中有一个小数点的错误,因为在某些情况下有一个快捷方式,当它是安全的而不是其他时候使用stuct,但是当包含小数的结构是一种情况时快捷方式不安全,被错误地识别为安全的情况。

答案 3 :(得分:3)

哈希码不可能在大多数非平凡类的所有变体中都是唯一的。在C#中,List相等的概念与Java中的概念不同(参见here),因此哈希代码实现也不相同 - 它反映了C#List的相等性。

答案 4 :(得分:3)

核心原因是性能和人性 - 人们倾向于认为哈希值很快,但通常需要至少遍历一次对象的所有元素。

示例:如果您使用字符串作为哈希表中的键,则每个查询都具有复杂度O(| s |) - 使用2x更长的字符串,它将花费您至少两倍的成本。想象一下,它是一个完整的树(只是一个列表列表) - oops: - )

如果完整,深度哈希计算是对集合的标准操作,那么很大比例的程序员会在不知情的情况下使用它,然后将框架和虚拟机归咎于缓慢。 对于像完全遍历一样昂贵的东西,程序员必须意识到复杂性是至关重要的。唯一要实现的就是确保你必须自己编写。这也是一个很好的威慑: - )

另一个原因是更新策略。每次计算和更新哈希与每次完整计算需要根据具体情况进行判断调用。

Immutabilty只是一个学术警察 - 人们将哈希作为一种更快地检测变化的方式(例如文件哈希),并且还使用哈希来处理一直在变化的复杂结构。 Hash在101个基础知识中有更多用途。 关键在于,复杂对象的哈希值必须根据具体情况进行判断。

使用对象的地址(实际上是一个句柄,因此它在GC之后不会改变)作为哈希实际上是哈希值对于任意可变对象保持相同的情况:-) C#的原因是它便宜且再次推动人们计算自己。

答案 5 :(得分:2)

你只是部分错了。当您认为相等的哈希码意味着相等的对象,但是相等的对象必须具有相同的哈希码时,你肯定是错的,这意味着如果哈希码不同,那么对象也是如此。

答案 6 :(得分:0)

为什么太哲学了。创建辅助方法(可能是扩展方法)并根据需要计算哈希码。可能是XOR元素的哈希码