实用技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > ASP.NET > 实用技巧 > .NET GetHashCode正确使用方式

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

作者: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关键字

为什么选17和31?

为什么每次都乘以31?

obj.userid?.GetHashCode() ?? 0

为什么要用三个字段?

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随便返回一个常数

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

误区3:不处理null值

最佳实践总结

  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正确使用方式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文