在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