在C#中判断两个对象是否相等,很多人习惯直接使用==运算符,但涉及引用类型、值类型、字符串以及Dictionary、HashSet等集合时,真正起作用的常常是Object基类提供的Equals和GetHashCode两个虚方法。理解它们的默认行为、重写规则和相互约束,是写好领域模型、避免哈希集合出现诡异问题的前提。本文从这两个方法的底层行为讲起,结合代码示例梳理常见错误和面试重点。

一、Object.Equals 的默认实现:引用相等与值相等
Object类的实例方法Equals本质上调用的是引用相等判断。对于没有重写Equals的普通类,两个不同实例即使所有属性字段一模一样,也会返回false,因为它们在堆上分配了不同的内存地址。这一点可以通过ReferenceEquals方法验证,ReferenceEquals永远只比较引用本身,不受任何重写影响。
值类型的表现则完全不同。ValueType重写了Equals,默认采用反射机制逐个字段比较,因此两个结构体实例只要所有字段值相同,Equals就会返回true。但这种反射比较的性能较差,而且某些字段类型可能存在精度或文化相关的不确定性,所以实际项目中自定义结构体通常建议重写Equals并提供强类型的比较逻辑。
string是一个看似例外的引用类型,它重写了Equals,比较的是字符内容而不是地址。也就是说两个内容相同的字符串即使不是同一个引用,Equals也会返回true。string同时还重写了GetHashCode,保证相等字符串产生相同的哈希码,这是它能够安全作为字典键的重要原因。下面示例展示了引用类型默认的相等性行为。
Person p1 = new Person { Name = "Tom", Age = 20 };
Person p2 = new Person { Name = "Tom", Age = 20 };
Console.WriteLine(p1.Equals(p2)); // False
Console.WriteLine(ReferenceEquals(p1, p2)); // False
string s1 = "abc";
string s2 = "abc";
Console.WriteLine(s1.Equals(s2)); // True
从输出结果可以看出,Person没有重写Equals,因此两个内容完全相同的对象被判定为不相等。而string因为重写了Equals,内容相同即可返回true。这个差异是理解对象相等性规则的第一步,也直接影响后续散列容器的使用。
二、GetHashCode 的作用与散列容器的一致性约束
GetHashCode方法返回一个int类型的哈希码,主要作用是为散列表容器提供快速定位能力。Dictionary、HashSet、Hashtable等集合内部并不是把所有元素线性排列,而是先根据键对象的哈希码计算桶位置,再到对应桶内调用Equals进行精确匹配。哈希码的质量和稳定性直接决定了散列容器的性能,甚至影响正确性。
GetHashCode有一条不可违反的核心约定:如果两个对象通过Equals比较相等,那么它们必须返回相同的哈希码。反过来不成立,不同对象允许返回相同哈希码,这就是哈希碰撞,虽然会降低性能,但不会破坏语义。另一个重要约束是哈希码在对象状态不变时应当保持稳定,如果对象在加入散列集合后哈希码发生变化,集合内部定位会错乱,元素可能无法被正常查找或删除。
默认情况下,引用类型的GetHashCode通常基于对象引用本身或同步块索引生成,因此两个内容相同的不同实例,其哈希码一般不同,这与引用类型默认的Equals语义保持一致。值类型则基于字段值计算,字段相同哈希码通常相同。下面代码演示了引用类型默认行为在字典中可能导致的问题。
Dictionary<Person, string> dict = new Dictionary<Person, string>();
Person key1 = new Person { Name = "Tom", Age = 20 };
dict[key1] = "first";
Person key2 = new Person { Name = "Tom", Age = 20 };
Console.WriteLine(dict.ContainsKey(key2)); // False
Console.WriteLine(dict[key2]); // KeyNotFoundException
在这个例子中,key1和key2虽然内容完全相同,但Person类没有重写Equals和GetHashCode,导致字典把key2当作不同的键。很多开发者在线下测试时用同一个引用作为键,没有暴露问题,一旦使用新建对象查找就会遇到KeyNotFoundException。这正是面试中常考的实际场景。
三、同时重写 Equals 和 GetHashCode 的正确姿势
当业务上需要按属性判断相等时,必须同时重写这两个方法。重写Equals时,通常先判断引用是否为同一个,再判断类型是否一致,最后逐字段比较;重写GetHashCode时,只使用参与相等性判断的字段参与哈希码计算。现代.NET推荐使用HashCode.Combine或System.HashCode结构来组合多个字段,这样比手动拼接数字更能降低碰撞概率。
下面的Person类实现了IEquatable<Person>接口,并同时重写了Equals和GetHashCode。强类型Equals方法避免了object.Equals中常见的装箱和类型检查,字典等泛型容器在支持该接口时会优先调用,从而提升性能。
public class Person : IEquatable<Person>
{
public string Name { get; set; }
public int Age { get; set; }
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
public bool Equals(Person other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
return Name == other.Name && Age == other.Age;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
}
重写之后,两个内容相同的Person对象会返回相同的哈希码,字典也能正确查找。但这里有一个极其重要的注意点:参与哈希码计算的字段必须是不可变的,或者至少保证对象加入散列集合后这些字段不再被修改。如果键对象的Name或Age在加入字典后发生了变化,哈希码就会跟着改变,字典无法再从原来的桶位置找到该条目,从而造成元素丢失。
常见错误之一是只重写Equals而不重写GetHashCode。编译器不会给出任何警告,但Dictionary和HashSet会违反契约。两个Equals相等的对象哈希码不同,字典可能同时存储多个“相同”的键,或者查找时因为定位到不同桶而失败。反过来只重写GetHashCode而不重写Equals也不行,因为哈希碰撞后还需要Equals确认,如果Equals仍是引用比较,内容相同的对象也不会被识别为相等。两者的重写必须成对进行。
四、面试高频追问与延伸:==、Equals、IEquatable 与静态方法
面试中经常问到Equals和==的区别。对于引用类型,==默认比较引用,但它是运算符,可以被重载;Equals是虚方法,可以在派生类中重写。string比较特殊,==和Equals都按内容比较,但==在双方都为null时也能返回true,而实例Equals则可能抛出NullReferenceException。对于值类型,==通常需要自定义运算符重载,否则可能无法直接使用,Equals则默认逐字段比较。回答这个问题时需要强调运算符与方法在语义上的不同。
IEquatable<T>接口提供强类型的Equals方法,有助于避免装箱和类型检查,尤其对值类型性能提升明显。泛型集合如Dictionary<TKey,TValue>和List<T>.Contains在条件允许时优先调用IEquatable<T>的实现。实际项目中通常会实现该接口,并让object.Equals委托给强类型版本,如前面示例所示。这也是体现代码质量的一个细节。
关于哈希码生成,面试官可能追问为什么不能只返回常量。如果所有对象哈希码都相同,散列表会退化为链表,查找性能从O(1)降到O(n)。还有可能追问为什么不能使用随机数,因为相等对象必须返回相同的哈希码,随机数无法保证这一约束。推荐使用HashCode.Combine或者质数相乘相加的传统方案,在简单与低碰撞之间取得平衡。哈希码不需要全局唯一,碰撞允许存在,最终相等性由Equals判断。
另一个容易忽视的点是静态Equals方法的空值安全性。Object.Equals(object objA, object objB)会先判断两个引用是否都为null,再调用实例Equals,因此可以安全处理null。而直接调用x.Equals(y)时,如果x为null会抛出NullReferenceException。这些细节虽然简单,却经常在面试中被用来考察候选人对.NET类型系统的理解深度。掌握这些规则后,再遇到哈希集合键对象、领域模型相等性设计等问题时,就能避免很多隐蔽缺陷。
C#对象相等性GetHashCodeEquals修改时间:2026-08-30 01:48:06