导读:本期聚焦于小伙伴创作的《C#怎么比较两个对象是否相等?Equals和GetHashCode重写方法详解》,敬请观看详情。在把自定义类型放进哈希集合时,常出现明明内容相同的对象却被判定为不同元素的情况。这源于默认Equals只比较引用地址,GetHashCode也未基于字段生成。要让两个属性一致的对象被视作相等,需同时重写Equals与GetHashCode,使相等对象拥有相同哈希值。本文说明值相等与引用相等的区别,给出重写步骤、示例代码及常见错误,比如只改Equals忘改GetHashCode会导致Dictionary查找失败。掌握这些能避免集合去重和映射出错。

在C#中,判断两个对象是否相等并不是简单用等号就能解决所有场景。对于引用类型,默认的比较行为往往只关心两个变量是否指向同一块内存地址;而在实际开发中,我们更常需要的是“逻辑相等”,也就是两个对象的关键属性值完全一致时就认为它们相等。要实现这种需求,就必须理解并正确重写Equals方法和GetHashCode方法。

C#怎么比较两个对象是否相等?Equals和GetHashCode重写方法详解

引用相等与值相等的区别

CLR(公共语言运行时)为所有类型提供了默认的相等判断逻辑。对于没有重写的引用类型,Object.Equals方法执行的是引用相等比较:只有当两个对象变量引用的是同一个实例时,才返回true。这意味着即使两个SeparateOrder对象的订单号、金额完全相同,只要它们是不同的new出来的实例,默认Equals就会说它们不相等。

与之相对的是值相等,即根据业务定义,两个对象的状态一致就视为相等。例如一个表示二维点的类,只要X和Y坐标相同,就应当被认为是同一个点。这种语义必须通过重写Equals来达成。同时,因为哈希表等集合依赖GetHashCode快速分流,所以也必须同步重写GetHashCode,否则会出现逻辑相等但哈希值不同的矛盾,导致集合行为异常。

为什么必须同时重写Equals和GetHashCode

在.NET的设计约定中,如果两个对象调用Equals返回true,那么它们的GetHashCode返回值必须相同。这个规则是哈希集合(如HashSet、Dictionary)正常工作的基础。哈希集合会先通过GetHashCode将对象分配到不同的桶中,再在桶内用Equals做精确比较。如果你只重写了Equals而没重写GetHashCode,两个逻辑相等的对象可能得到不同的哈希码,被放进不同的桶,集合就会认为它们是不重复的元素。

反过来说,GetHashCode相等并不要求Equals一定相等(这叫哈希冲突,是允许的),但Equals相等则强制要求哈希码相等。因此,任何对Equals的自定义重写,都必须搭配GetHashCode的重写,且哈希算法要基于参与相等比较的那些字段。漏掉这一步是在使用自定义类型作为字典键或集合元素时最常见的隐蔽Bug来源。

重写Equals的标准写法

重写Equals时,需要考虑空值、类型匹配以及字段逐一比较。建议先检查引用相等以提升性能,再处理null和类型,最后比较业务字段。下面以一个表示用户的类为例,当用户名和邮箱都相同时视为相等。

using System;

public class User
{
    public string Name { get; set; }
    public string Email { get; set; }

    public override bool Equals(object obj)
    {
        // 引用相等直接返回true
        if (ReferenceEquals(this, obj))
        {
            return true;
        }
        // 空或类型不同返回false
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        User other = (User)obj;
        // 比较关键字段,使用string.Equals处理null安全
        return string.Equals(Name, other.Name) &&
               string.Equals(Email, other.Email);
    }

    public override int GetHashCode()
    {
        // 基于参与相等的字段生成哈希
        int hash = 17;
        hash = hash * 31 + (Name == null ? 0 : Name.GetHashCode());
        hash = hash * 31 + (Email == null ? 0 : Email.GetHashCode());
        return hash;
    }
}

上面的代码展示了典型实现:先通过ReferenceEquals做快捷判断,再用GetType确保类型完全一致(不使用is操作符是为了防止派生类被误判为基类相等)。字段比较使用string.Equals避免空引用异常。这种写法清晰且安全,适合大多数业务模型。

如果你使用的是C# 9及以上版本,记录类型(record)已经自动帮我们实现了基于所有属性的值相等和哈希码,但对于普通class仍需手动处理。另外,实现IEquatable<T>接口可以提供一个强类型的Equals,避免装箱并提升性能,但不是必须步骤。

GetHashCode的实现要点与陷阱

GetHashCode的目标是把对象均匀映射到整数空间,且相同逻辑对象必须得到相同值。一个简单可靠的做法是选用一个素数作为初始值,然后对每个参与相等的字段乘以另一个素数并累加其哈希码。如前面示例中的17和31组合,能减少冲突。

需要特别注意:GetHashCode返回值在对象生命周期内如果作为哈希集合的键,则不应改变。也就是说,参与哈希计算的字段最好是只读的,或者在对象放入集合后不要再修改。否则原哈希桶将失效,导致无法从Dictionary中查到该对象。下面演示一个错误的可变字段哈希案例:

public class BadKey
{
    public string Code { get; set; }

    public override int GetHashCode()
    {
        // 如果Code在放入字典后被修改,哈希码变化,字典找不到
        return Code == null ? 0 : Code.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj is BadKey other)
        {
            return string.Equals(Code, other.Code);
        }
        return false;
    }
}

在这个错误示例中,一旦BadKey实例的Code属性被改动,之前存入Dictionary的条目就会“丢失”。因此设计键类型时,优先使用不可变属性,或者在文档中明确约定放入集合后禁止修改。

使用场景与最佳实践总结

当你需要把自定义对象用作HashSet元素、Dictionary的键,或者在LINQ中使用Distinct、Contains等依赖相等语义的操作时,重写Equals和GetHashCode就是必选项。如果只是普通业务逻辑里偶尔比一下,也可以不重写而编写专门的比较器(IEqualityComparer),这样能把比较逻辑与类型本身解耦。

总结起来,正确的做法包括:Equals与GetHashCode成对重写;哈希计算覆盖所有相等判断字段;优先保证键的不可变性;类型检查用GetType而非is以避免继承歧义。遵循这些原则,你就能在C#中准确控制对象相等的含义,避免集合类出现难以排查的错误。

C#EqualsGetHashCode修改时间:2026-08-06 02:57:18

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。