特征码的使用办法_C#-关于GetHashCode的使用准则(转载+翻译)。

1.GetHashCode的作用是什么?

根据其设计来说,仅仅是将一个对象放入哈希表中。

2.在Object类中,为什么首先要有GetHashCode方法?

正常情况下,每一个类型中都会有一个GetType方法;在CLR(Common Language Runtime)工作系统中比较关键的特征是数据具有描述自身的能力,例如一个Person类中可以包含Name、Age等描述型属性。C#所有类的基类是Object类,每一个派生自Object的类都拥有ToString()方法,这会更加方便我们进行信息打印,和功能调试。同时一个Object与其他Object进行相等性比较也是一件有意义的事情。以上是一个Object类中的几个常见方法,回到本次我们要讲的主题GetHashCode方法,GetHashCode也是Object中一个方法,其目的是将类型对象转换成哈希码,并以散列的形式存储至哈希表中。那么问题来了,为什么要这么做呢?

如果现在需要我们重新设计一个类型系统,那么哈希散列可能会有不同的实现方式,比如说实现一个IHashable接口。但是在设计CLR类型系统时还没有泛型类型的概念,所以选择用哈希表可以存储任何对象类型。

3.HashTables和类似HashTables的数据结构应该如何使用GetHashCode?

这里假设我们现在拥有一个名为“set”的泛型抽象数据类型:Set<T>。在这个类型中两个基本的功能是插入行为和包含检查行为。下面使用列表来对这个集合进行具体的实现。

public class Set<T>
{

  private List<T> list = new List<T>();

  public void Insert(T item)
  {
    if (!Contains(t))
      list.Add(item);
  }

  public bool Contains(T item)
  {
    foreach(T member in list)
      if (member.Equals(item))
        return true;
    return false;
  }
}

注意:(上面代码块中省略了一些异常检查,例如:需要保证类型不为空)

根据以上代码可以看出,我们每次调用Insert方法的时候,都会调用一次Contains方法进行重复性检查,这种情况下如果列表中有一万个对象,那么当我们插入新数据的时候必须遍历检查一遍所有一万个对象,以确定该对象不在列表中。 这是一件可怕的事情,解决问题的方法是牺牲少量的内存来提高整体检查速度。具体流程:

(1).使用GetHashCode获取需要存储对象的哈希值。

(2).引入"Bucket(桶)"的概念,Bucket可以是一个列表,可以是一个数组。

(3).哈希值和Bucket的长度求余得出当前需要存储条目位于哪一个桶中。(求余本身就是一个很好的散列函数,哈希函数其实就是散列函数,当然也包括求余)

(4).定位到指定桶中存储当前对象。

Public class Set<T>
{
  private List<T>[] buckets = new List<T>[100];

  public void Insert(T item)
  {
    int bucket = GetBucket(item.GetHashCode());
    if (Contains(item, bucket))
      return;
    if (buckets[bucket] == null)
      buckets[bucket] = new List<T>();
    buckets[bucket].Add(item);
  }

  public bool Contains(T item)
  {
    return Contains(item, GetBucket(item.GetHashCode());
  }

  private int GetBucket(int hashcode)
  {
    unchecked
    {
      // A hash code can be negative, and thus its remainder can be negative also.
      // Do the math in unsigned ints to be sure we stay positive.
      return (int)((uint)hashcode % (uint)buckets.Length);
    }
  }

  private bool Contains(T item, int bucket)
  {
    if (buckets[bucket] != null)
      foreach(T member in buckets[bucket])
        if (member.Equals(item))
          return true;
    return false;
  }
}

现在,如果我们有一万个对象需要存储,那么我们正在寻找一百个桶中的一个,每个桶平均有一百个对象。 与之前相比查找的代价降低了100倍。存储桶的集合也可以像List<T>在变满时进行扩容一样,可以调整其容量大小(保证不会有过多的闲置的桶,这样会造成资源的浪费,比如明明2个桶可以存储的数据,却要定义一个长度为20的桶)。对于桶的长度,我们通常会选择质数,比如:1,3,5,7,9,11......... 。

从上面代码所起到的作用来说,我们可以推断出一些有关GetHashCode的规则和准则:

规则1:两个对象相等,它们的哈希值相等。

如果两个对象相等,则它们必须具有相同的哈希值。等价于,如果两个对象具有不同的哈希值,则它们一定是不相等的。

理解这个规则其实很简单,这里我们假设有两个相等的对象,它们分别是Object A和Object B 但是Object A HashCode不等于Object B HashCode。 在这种条件下,如果我们将Object A存储至编号为12的Bucket中=>Buckets[12],之后如果我们再此访问集合来检测Object B是否为此集合的成员,那么由于两个对象的哈希值不同,所以此时有可能访问到的Bucket的编号为67=>Buckets[67],本来两个对象是相等的,但是由于哈希值不同,所以通过B并不能检索到A的存在。这违背了我们上述的规则。

注意注意!!!

这里我们将情况反转一下,假设两个对象具有相同的哈希值,那么他们代表的对象可能是不相等的,哈希码大概只有40亿个,但是显然存在超过40亿个对象的可能性,这可能会造成哈希值不够用。 一个仅拥有十个字符的字符串就远远超过40亿个。所以,根据鸽巢原理(也称作抽屉原理)来说一定会存在至少有两个不相同的对象共享相同的哈希码的情况。

抽屉原理(名词)_百度百科​baike.baidu.com
81b69f8d00f538a3d477f20916b49673.png

我们最后整理一下此处的结论:

1.A=B=>A HashCode = B HashCode(A等于B,那么其哈希值一定相等)

2.A HashCode = B HashCode(不一定)=>A=B(哈希值相等,A和B不一定相等)

3.A HashCode != B HashCode=>A!=B(哈希值不等,那么A和B一定不相等)

4.A!=B=>A HashCode(不一定)! = B HashCode(A和B不相等,其哈希值不一定不相等)

规则2:当一个数据结构中使用哈希码来作为对象的表示时,每次调用GetHashCode方法返回的整型哈希码要确保相同。

正常情况下,一个可变的对象,其哈希码的值只能由不能突变的字段提供,以此来保证哈希码在整个生命周期中不会发生改变。

想让对象的哈希码随着对象字段的变化而发生改变当然是可以的,但是这是很危险的。如果我们有一个需要放在哈希表中的对象,那么哈希码改变和哈希表维护这两部分的代码功能实现必须具有一些已经达成共识的协议。

如果对象的哈希码在哈希表中是可以发生变化的,那么Contains方法肯定会出现问题。假如我们把对象存储进编号为5的Bucket中,此时由于我们的代码设定,使得对象原本的哈希值发生了改变,当我们再此访问集合是否已经存在此对象,根据新的哈希值计算它可能会存储在编号为74的Bucket中。结果显而易见,我们无法查找到此对象。

我们需要明白的是,对象会以我们意想不到的一些方法存储起来。 许多LINQ序列运算在内部使用的哈希表,当我们通过LINQ不断枚举返回要查询的对象时,不能进行一些比较危险的值修改行为!!!

规则3:在不同时间或不同应用域(APPDomains)中使用GetHashCode时,没有办法保证值不变。

现在假设我们有一个对象“A”,A中包含了“Name、Address“等字段,如果我们在两个不同的进程中创建两个数据完全相同的A对象:A1(Name:XiaoMing;Address:XiBei)和A2(Name:XiaoMing;Address:XiBei),在这种情况下A1、A2不必有相同的哈希码。如果本周二我们在一个进程中创建了对象A,然后关闭进程。之后在周三再此运行该程序,那么当我们查看其对象哈希值的时候他们可能是不同的,这种情况在过去是发生过的,在文档的System.String.GetHashCode中有特别指出:在不同的CLR版本中,两个相同的字符串可以具有不同的哈希码,实际上它们确实具有不同的哈希码。所以综上所述我们应该注意到的是,不要在数据库中存储字符的哈希码并期望它们永远相同,因为它们不会永远相同。

规则4:GetHashCode方法规定不能抛出异常,但是一定要有返回值(哈希码)。

获取哈希码的过程只是一个计算整数的过程,我们没有理由让它在计算过程中抛出失败异常。GetHashCode方法的实现应该能够处理对象的任何合法配置。我偶尔会得到响应“但我想在我的GetHashCode中抛出NotImplementedException,以确保该对象永远不会被放入散列表;我不打算把这个对象放到哈希表中"好的,但是前面的指导方针的最后一句话是适用的;这意味着您的对象不能成为许多在内部为performan使用散列表的LINQ-to-objects查询的结果

规则5:GetHashCode的实现一定要保证其获取速度够快。

GetHashCode的目的是优化查找操作,如果对GetHashCode的调用比查看对象一万次还要慢,显然我们是没有获得性能收益的。

”随机分布”是指:如果这些对象使用哈希函数进行随机分布,那么在产生的哈希码中就不应该有相似的共性。例如,假设您正在使用哈希函数计算一个表示点的经度和纬度的对象。一组这样的经纬度位置很可能是比较“聚集的”;比如说,你所在的地点大部分是在同一个城市的房子,这种可能性很大。如果群集数据产生群集哈希值,那么这可能会减少对不同编号存储桶(Bucket)的使用数量,这可能会导致较少的桶被使用,同时每个桶里面又存储着大量数据,很明显的性能问题。

转自:

Guidelines and rules for GetHashCode​blogs.msdn.microsoft.com
c6f22190f2565226db1eaa4a82c5b937.png

相关水平有限,大家凑活看,会不定时完善。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值