Understanding C# Object Equals: Reference vs Value Equality
c# object equals: Learn how object.Equals works in C#, when to override it, and how to implement value equality correctly with GetHashCode and IEquatable<T>.
In C#, the default behavior of object.Equals is to perform reference equality for classes. That means two variables are considered equal only if they point to the same instance in memory. For many domain objects, this is not the intended comparison. When you need two separate instances with the same field values to compare as equal, you must override Equals and provide your own definition of value equality. This article explains the mechanics of c# object equals, how to override it correctly, and the pitfalls that commonly trip up developers.
What object.Equals Does by Default
The object class provides a virtual Equals(object obj) method. For reference types, the default implementation uses reference equality: it returns true only when both operands refer to the exact same object instance. For value types, the default behavior is different: it compares the values of all fields using reflection, which is slow and can produce inconsistent results if the struct contains reference-type fields. In practice, you rarely rely on the default for structs because of the performance cost and the lack of control.
public class Person { public string Name { get; set; } public int Age { get; set; } } var p1 = new Person { Name = "Alice", Age = 30 }; var p2 = new Person { Name = "Alice", Age = 30 }; Console.WriteLine(p1.Equals(p2)); // False
Here p1 and p2 are different instances, so the default Equals returns false even though their property values match.
Value Equality vs Reference Equality
Reference equality answers the question: "Are these two variables pointing to the same object?" Value equality answers: "Do these two objects contain the same data?" The distinction matters in collections, dictionaries, and LINQ operations. For example, List<T>.Contains and Distinct rely on Equals to determine membership. If you do not override Equals, a list of Person objects will treat two identical records as distinct entries, which often leads to bugs.
Value equality is also essential when using types as dictionary keys. The Dictionary<TKey, TValue> uses both GetHashCode and Equals to locate entries. If two keys are logically equal but produce different hash codes or fail the Equals check, the dictionary will not find the value you expect.
Overriding Equals for Value Semantics
To implement value equality, override the Equals method and the GetHashCode method together. The Equals method should follow a consistent pattern:
- Check for null.
- Check for reference equality as a fast path.
- Check that the argument is of the same type (or a compatible type).
- Compare each relevant field.
public class Person { public string Name { get; set; } public int Age { get; set; } public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; var other = (Person)obj; return Name == other.Name && Age == other.Age; } }
Note the use of GetType() to ensure the object is exactly the same type, not a derived type. If you want derived types to compare equal when they share the same base properties, you might use as and then check for null, but that can lead to asymmetric equality. The safest approach is to require the exact same type unless you have a well-defined inheritance hierarchy.
The Relationship Between Equals and GetHashCode
When you override Equals, you must also override GetHashCode. The rule is simple: if two objects are equal according to Equals, they must produce the same hash code. If they do not, collections like HashSet<T> and Dictionary<TKey, TValue> will behave incorrectly. A common implementation combines the hash codes of the fields that participate in equality.
public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 23 + (Name?.GetHashCode() ?? 0); hash = hash * 23 + Age.GetHashCode(); return hash; } }
The unchecked block prevents overflow exceptions during multiplication. The prime numbers (17 and 23) help reduce hash collisions. You should use the same fields in GetHashCode as you do in Equals. If you change a field after the object has been added to a hash-based collection, the hash code changes and the object becomes unreachable. That is why it is best to use immutable fields for equality.
Implementing IEquatable<T> for Performance
The object.Equals method takes an object parameter, which causes boxing when you compare value types. To avoid that overhead and to provide a strongly typed comparison, implement IEquatable<T>. This interface defines an Equals(T other) method that avoids casting and boxing.
public class Person : IEquatable<Person> { public string Name { get; set; } public int Age { get; set; } public bool Equals(Person other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Name == other.Name && Age == other.Age; } public override bool Equals(object obj) { return Equals(obj as Person); } public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 23 + (Name?.GetHashCode() ?? 0); hash = hash * 23 + Age.GetHashCode(); return hash; } } }
Now the generic Equals is used by collections like List<T> and Dictionary<TKey, TValue> when the type is known at compile time. This avoids the overhead of casting to object and back, which matters in tight loops or when comparing many structs.
Common Pitfalls When Overriding Equals
One frequent mistake is forgetting to override GetHashCode. The compiler does not enforce it, but the runtime behavior will be incorrect. Another issue is using mutable fields in equality. If a field used in Equals changes after the object is placed in a HashSet, the set will no longer be able to find that object. Similarly, overriding Equals without also overriding the == and != operators can lead to surprising inconsistencies. In C#, the == operator is not automatically tied to Equals. If you want == to use your value semantics, you must overload it.
public static bool operator ==(Person left, Person right) { if (left is null) return right is null; return left.Equals(right); } public static bool operator !=(Person left, Person right) { return !(left == right); }
Another subtle issue is asymmetry. If you implement Equals to accept a base type, then base.Equals(derived) and derived.Equals(base) may give different results. The safest pattern is to require the exact same type, as shown earlier.
Performance and Allocation Considerations
Default Equals for structs uses reflection, which is extremely slow compared to a hand-written implementation. If you have a struct that is frequently compared, implement IEquatable<T> and override GetHashCode to avoid the reflection overhead. For classes, the default reference equality is fast, but once you override Equals, you introduce a method call and field comparisons. That is usually negligible, but if you are comparing millions of objects, consider whether value equality is truly needed or whether you can use a more efficient key.
Boxing is another concern. When you call Equals(object) on a value type, the value is boxed, allocating a new object on the heap. Using IEquatable<T> avoids that allocation. In high-throughput scenarios, this can reduce garbage collection pressure significantly.
When to Use object.ReferenceEquals
There are cases where reference equality is the correct behavior. For example, when you want to ensure two variables point to the same instance, or when you are implementing a singleton pattern. object.ReferenceEquals is also useful inside an overridden Equals as a fast path: if both references are the same, you can return true without comparing fields. It is also the only way to compare two objects without invoking their overridden Equals, which can be useful in diagnostic code or when you intentionally want to ignore value semantics.
if (object.ReferenceEquals(x, y)) { // Same instance, no need to compare fields. }
Remember that ReferenceEquals works for reference types. For value types, it always returns false because boxing creates a new instance each time. Use it only where reference semantics are meaningful.
Maintaining Consistency in Derived Types
When you have a class hierarchy, equality becomes more complex. If a base class overrides Equals, derived classes must decide whether to extend that behavior or require the same type. A common approach is to make the base Equals virtual and let derived classes call base.Equals and add their own fields. However, this can break symmetry if the derived type is compared to the base type. To avoid this, many developers require exact type equality in the base class, which prevents derived instances from ever being equal to base instances. That is a safe, predictable design, but it means a derived object will never equal a base object even if all shared fields match. Choose the strategy that fits your domain and document it clearly.
A more robust alternative is to use a unique type identifier, such as a Type field, in the equality check. But this adds complexity. For most applications, the exact-type check is sufficient and easier to reason about.