一文详解.NET中GetHashCode方法的正确使用方式

 更新时间:2026年05月22日 09:03:17   作者:YahirQ  
当你看到 Distinct 去重结果不符合预期时,很可能不是因为数据有问题,而是因为 IEqualityComparer 中的 GetHashCode 实现出错了,本文从一个真实代码片段出发,深入剖析哈希码的原理、书写规范,并给出正确且高性能的实现方式,需要的朋友可以参考下

一、一段“诡异”的去重代码

先看下面这段代码,它尝试对 hr_data_approve 对象集合按 useridbegintimeendtime 三个字段的组合进行去重:

public class ApproveDistinctCompare : IEqualityComparer<hr_data_approve>
{
    public bool Equals(hr_data_approve x, hr_data_approve y)
    {
        return x.begintime == y.begintime && x.endtime == y.endtime;
    }
    public int GetHashCode([DisallowNull] hr_data_approve obj)
    {
        return obj.userid.GetHashCode();
    }
}
leaveList = leaveList.Distinct(new ApproveDistinctCompare()).ToList();

乍一看似乎没什么问题,但实际运行后去重结果完全不符合预期:有时本该去重的记录留了下来,有时不该去重的却被删掉了。原因就藏在 GetHashCodeEquals不一致上。

二、HashCode是什么?为何如此重要?

2.1 哈希码的定义

GetHashCode 返回一个 int 类型的数值,可以理解为对象的“短指纹”。它的核心作用是为基于哈希表的集合(如 HashSet<T>Dictionary<TKey,TValue>LINQDistinct() 等)提供快速定位能力

哈希表的工作原理大致是:

  1. 插入元素时,先调用 GetHashCode 得到哈希码,通过 hashCode % bucketCount 决定将元素放入哪个“桶”。
  2. 查找时,同样计算哈希码,直接定位到对应的桶,然后在桶内使用 Equals 逐个比较。

2.2 哈希码的黄金法则

如果 Equals(a, b) 返回 true,那么 a.GetHashCode() 必须等于 b.GetHashCode()
反之不要求(不同对象可以哈希码相同,这叫“碰撞”)。

违反这一法则,哈希表的行为会变得完全不可预测——因为两个“相等”的对象可能被分配到不同的桶,导致 Equals 永远不会被调用,从而误判为不相等。

三、原代码错在哪?

方法期望(场景二)原代码实现后果
Equals比较 useridbegintimeendtime只比较 begintimeendtime不同用户只要时间相同就被误判为相等
GetHashCode基于三个字段计算只基于 userid 计算相同用户不同时间的对象哈希码相同,导致大量碰撞,性能下降,且与 Equals 逻辑割裂

更严重的是,由于 EqualsGetHashCode 用的字段完全不同,违反了哈希码黄金法则:两个具有相同 begintime/endtime 但不同 userid 的对象,Equals 返回 true,而 GetHashCode 因为 userid 不同而返回不同值。这会让 Distinct 内部判断逻辑混乱,去重结果随机错误。

四、正确的GetHashCode应该怎么写?

针对场景二(基于 userid、begintime、endtime 三者组合去重),正确的实现如下:

public class ApproveDistinctCompare : IEqualityComparer<hr_data_approve>
{
    public bool Equals(hr_data_approve x, hr_data_approve y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return x.userid == y.userid 
            && x.begintime == y.begintime 
            && x.endtime == y.endtime;
    }
    public int GetHashCode([DisallowNull] hr_data_approve obj)
    {
        if (obj is null) throw new ArgumentNullException(nameof(obj));
        unchecked
        {
            int hash = 17;
            hash = hash * 31 + (obj.userid?.GetHashCode() ?? 0);
            hash = hash * 31 + (obj.begintime?.GetHashCode() ?? 0);
            hash = hash * 31 + (obj.endtime?.GetHashCode() ?? 0);
            return hash;
        }
    }
}

4.1 代码逐行解读

unchecked关键字

  • 哈希计算涉及乘法,int 很容易溢出。unchecked 允许溢出时自动回绕(wrap around),这是哈希算法的正常现象,无需抛出异常。

为什么选17和31?

  • 这两个数是素数,使用素数可以降低哈希碰撞的概率。
  • 31 是经典的乘数(Java 的 Objects.hash() 也用 31),因为 31 * i 可被 JIT 优化为 (i << 5) - i,位运算效率高。

为什么每次都乘以31?

  • 避免不同字段顺序产生相同的哈希值。比如 ("A","B")("B","A") 如果仅累加,会得到相同结果;而乘以质数再加新字段,可以让顺序影响最终哈希值。

obj.userid?.GetHashCode() ?? 0

  • 处理字段可能为 null 的情况:当 useridnull 时,?. 阻止调用 GetHashCode,表达式返回 null?? 0 将其替换为 0。这样既安全又不会抛空引用异常。

为什么要用三个字段?

  • 必须与 Equals 基于完全相同的字段组合。因为 Equals 判断三个字段全部相等才返回 true,所以只有当三个字段都相同时,哈希码也必须相同。这是契约的要求。

4.2 简化写法(.NET Core 2.1+)

如果你使用的框架版本支持 System.HashCode,可以大幅简化:

public override int GetHashCode() => HashCode.Combine(userid, begintime, endtime);

VSCode / Visual Studio 还提供了自动生成 EqualsGetHashCode 的功能(右键 → 快速操作 → 生成 Equals/GetHashCode),非常推荐使用。

五、常见误区与最佳实践

误区1:只在Equals里写逻辑,GetHashCode随便返回一个常数

  • 后果:所有对象哈希码相同,全部落入同一个桶,哈希表退化成链表,性能从 O(1) 变成 O(n)。

误区2:用可变字段参与哈希码计算

  • 后果:对象加入 HashSet 后,如果参与哈希码的字段被修改,该对象在哈希表内的位置就会“丢失”,再也无法被查找或删除。推荐仅用不可变字段(如 Id、创建时间)计算哈希码

误区3:不处理null值

  • 后果:当字段为 null 时调用其 GetHashCode 会抛出 NullReferenceException。始终使用 ?.GetHashCode() ?? 0 或显式判断。

最佳实践总结

  1. EqualsGetHashCode 必须基于完全相同的字段组合
  2. 使用质数(如 17、31)作为初始值和乘数,降低碰撞率。
  3. unchecked 处理溢出。
  4. 处理可能为 null 的字段。
  5. 优先使用 HashCode.Combine 或 IDE 生成工具。
  6. 哈希码计算中使用的字段应为只读(或至少不应在对象作为哈希表键时被修改)。

六、结语

GetHashCode 看起来只是简单的整数计算,但它与 Equals 共同构成了 .NET 中所有哈希集合的基石。一个小小的不一致,就可能让 DistinctHashSetDictionary 出现匪夷所思的错误。下次再遇到“明明数据一样,为什么去重无效”的问题,请第一时间检查 GetHashCodeEquals 是否“言行一致”。

记住黄金法则

// 如果以下代码输出 true,那么 hash1 必须等于 hash2
bool equal = comparer.Equals(a, b);
int hash1 = comparer.GetHashCode(a);
int hash2 = comparer.GetHashCode(b);

希望这篇文章能帮你彻底理解哈希码,写出健壮、高效的自定义比较器。

以上就是一文详解.NET中GetHashCode方法的正确使用方式的详细内容,更多关于.NET GetHashCode正确使用方式的资料请关注脚本之家其它相关文章!

相关文章

  • 在一个网站下再以虚拟目录的方式挂多个网站的方法

    在一个网站下再以虚拟目录的方式挂多个网站的方法

    在一个网站下再以虚拟目录的方式挂N个网站的方法
    2010-04-04
  • 加密web.config的方法分享

    加密web.config的方法分享

    加密web.config的方法分享,需要的朋友可以参考一下
    2013-03-03
  • asp.net(C#)遍历memcached缓存对象

    asp.net(C#)遍历memcached缓存对象

    出于性能考虑,memcached没有提供遍历功能,不过我们可以通过以下两个stats命令得到所有的缓存对象。
    2010-03-03
  • .net core api接口JWT方式认证Token

    .net core api接口JWT方式认证Token

    本文详细讲解了.net core api接口JWT方式认证Token,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • ASP.NET用户控件技术

    ASP.NET用户控件技术

    ASP.NET用户控件技术...
    2007-04-04
  • asp.net 细说文件读写操作(读写锁)

    asp.net 细说文件读写操作(读写锁)

    开发过程中,我们玩玩需要大量与文件交互,读文件,写文件已成家常便饭,本地运行完美,但一上到投产环境,往往会出现很多令人措手不及的意外,或开发中的烦恼,因此,我对普通的C#文件操作做了一次总结
    2011-12-12
  • ASP.NET实现Hadoop增删改查的示例代码

    ASP.NET实现Hadoop增删改查的示例代码

    本篇文章主要介绍了ASP.NET实现Hadoop增删改查的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 在ASP.NET中读写XML数据的多种方法

    在ASP.NET中读写XML数据的多种方法

    在ASP.NET日常开发中,XML(可扩展标记语言)是一种常用的数据交换格式,它被广泛用于配置文件、数据传输和Web服务等场景,在.NET框架中,提供了多种类和方法来读写XML数据,以下是对ASP.NET中读写XML的详解,需要的朋友可以参考下
    2025-01-01
  • JWT + ASP.NET MVC时间戳防止重放攻击详解

    JWT + ASP.NET MVC时间戳防止重放攻击详解

    这篇文章主要给大家介绍了关于JWT + ASP.NET MVC时间戳防止重放攻击发的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-07-07
  • 浅析.net core 抛异常对性能影响

    浅析.net core 抛异常对性能影响

    在.net项目中使用自定义异常来处理业务很爽,但是又担心大量抛业务异常存在性能问题,下面通过本文介绍.net core 抛异常对性能影响的求证之路,需要的朋友可以参考下
    2022-06-06

最新评论