What Happens If You Override Equals() but Not GetHashCode() in C#
When working with collections like HashSet or Dictionary<TKey, TValue>, understanding the relationship between Equals() and GetHashCode() is critical. Many hidden bugs and performance problems in .NET apps happen because these two methods are used incorrectly.
Let’s unpack this in detail step by step.
The Role of Equals() and GetHashCode()
Every object in C# inherits two fundamental methods from System.Object:
- public virtual bool Equals(object obj);
- public virtual int GetHashCode();
They work hand-in-hand to determine object equality and hash identity.
-
Equals() defines logical equality — when two objects mean the same thing.
-
GetHashCode() provides a hash used for fast lookups in hash-based collections (like HashSet, Dictionary and ConcurrentDictionary).
How Hash-Based Collections Work
Collections like HashSet and Dictionary<TKey, TValue> are hash-based. They use a two-step process for lookup and storage:
Hashing Phase:
- Call GetHashCode() to compute an integer hash.
- This determines which bucket an object belongs to.
Equality Phase:
- Once inside the right bucket, call Equals() to check if the item is actually equal to an existing entry.
Example: HashSet Internals (Simplified)
Imagine a HashSet as multiple buckets (like small lists):
Bucket 0: [ ]
Bucket 1: [ ]
Bucket 2: [Person("Alice")]
Bucket 3: [ ]
Bucket 4: [Person("Bob")]
When you add new Person("Alice"):
-
The hash code decides which bucket to use.
-
Then Equals() ensures no duplicates exist in that bucket.
If GetHashCode() returns different values for logically equal objects, they end up in different buckets and Equals() never even gets called to compare them.
This is the Problem when Overriding Equals() but Not GetHashCode()
Consider this class:
class Person
{
public string Name { get; set; }
public override bool Equals(object obj)
{
return obj is Person other && Name == other.Name;
}
// ❌ Forgot to override GetHashCode()
}
Let’s test it:
var set = new HashSet<Person>();
set.Add(new Person { Name = "Dev" });
set.Add(new Person { Name = "Dev" });
Console.WriteLine(set.Count); // Output: 2
Even though both objects are logically equal (Equals() returns true), HashSet stores both, because they have different hash codes.
Why It Happens
By default, GetHashCode() (from object) uses the object's reference in memory. So, two different instances even with identical content, produce different hash codes.
Since the hash codes are different, the objects might end up in different buckets (bucketIndex = hashCode % capacity). In that case, the HashSet won’t even call Equals() to compare them. It only calls Equals() if both objects fall into the same bucket.
The Object Equality Contract
Microsoft’s official guidelines define a clear contract between Equals() and GetHashCode():
If two objects are equal (according to Equals()),
they must return the same value from GetHashCode().
Violating this rule leads to unpredictable behavior in any hash-based collection.
The Rules Summarized
| Rule | Description |
|---|---|
| Reflexive | x.Equals(x) must be true. |
| Symmetric | x.Equals(y) must equal y.Equals(x). |
| Transitive | If x.Equals(y) and y.Equals(z), then x.Equals(z) must be true. |
| Consistency | Repeated calls must return the same result unless the object changes. |
| Hash Code Contract | If x.Equals(y) is true, then x.GetHashCode() == y.GetHashCode() must be true. |
.
Correct Implementation Example
Here’s how to fix it properly:
class Person
{
public string Name { get; set; }
public override bool Equals(object obj)
{
return obj is Person other && Name == other.Name;
}
public override int GetHashCode()
{
// Null-safe hash computation
return Name?.GetHashCode() ?? 0;
}
}
Now test again:
var set = new HashSet<Person>();
set.Add(new Person { Name = "Dev" });
set.Add(new Person { Name = "Dev" });
Console.WriteLine(set.Count); // ✅ Output: 1
Now Equals() and GetHashCode() are consistent both objects hash to the same bucket and the duplicate is detected.
Real-World Impact
Here’s what can break if you don’t override both:
| Collection | Problem |
|---|---|
| HashSet | Stores duplicates that should be equal. |
| Dictionary<TKey, TValue> | Treats equal keys as distinct, causing overwriting or lookup failures. |
| ConcurrentDictionary<TKey, TValue> | Thread-safe version suffers the same issue. |
| LINQ | Some LINQ methods (like Distinct()) rely on hash codes and may misbehave. |
| Contains(), Remove() | Fail to find existing items that are considered “equal.” |
Performance Implications
Even if things seem to work, a broken hash implementation can:
-
Dramatically increase lookup time (because objects go into wrong buckets)
-
Cause memory waste (duplicate entries)
-
Lead to unpredictable behavior across different .NET versions or runtime optimizations
-
Hash-based collections rely on hash uniformity for performance — mismatched hashes kill that.
How to Implement GetHashCode() Safely
A robust hash code implementation should:
-
Use immutable fields (values that don’t change after construction)
-
Combine multiple fields using a reliable hash combiner
-
Avoid overflow or randomization issues
Example:
class Employee
{
public int Id { get; set; }
public string Department { get; set; }
public override bool Equals(object obj)
{
return obj is Employee e && Id == e.Id && Department == e.Department;
}
public override int GetHashCode()
{
return HashCode.Combine(Id, Department);
}
}
.NET’s HashCode.Combine() (C# 8+) simplifies safe, high-quality hash generation.
Common Misconceptions
| Myth | Reality |
|---|---|
| "Equals() is enough to define equality." | Not for hash-based collections - you need consistent hashes. |
| "GetHashCode() must be unique." | No , only consistent. Collisions are okay; inconsistency isn’t. |
| "You must override Equals() and GetHashCode() together." | Always. They’re inseparable for logical equality. |
| "Changing hash logic later is safe." | Not for persisted or serialized collections. it may corrupt lookups. |
Summary
| Concept | Explanation |
|---|---|
| Equals() | Defines logical equality between objects. |
| GetHashCode() | Determines hash identity for collections. |
| If only Equals() is overridden | Hash-based collections misbehave (duplicates, failed lookups). |
| If both are overridden correctly | Consistent, predictable equality semantics. |
| Golden Rule | Equal objects must have equal hash codes. |
In Short
If you override Equals(), you must override GetHashCode().
Failing to do so may not crash your program, but it will silently break correctness in ways that are hard to debug.
Example Summary in One Line
HashSet won’t even check equality between two objects unless their hash codes match.
