Back to Blog
C#

C# GetHashCode: Contract and Correct Overrides

c# gethashcode: Learn how GetHashCode works in C#, why it matters for collections, and how to override it correctly without breaking equality.

GetHashCodeEqualityDictionaryHashSetOverridePerformance
Illustration of a C# object being hashed into a bucket array in a dictionary.

c# gethashcode requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, GetHashCode is the method that determines which bucket an object lands in when you use it as a key in a Dictionary or add it to a HashSet. Its correctness directly affects the behavior of these collections. If you override Equals without also overriding GetHashCode, or if you implement GetHashCode incorrectly, lookups can fail, throw exceptions, or degrade to O(n) performance.

Why GetHashCode Matters in C#

Hash-based collections rely on a two-step lookup. First, the collection calls GetHashCode on the key to compute a bucket index. Then, it calls Equals on each object in that bucket to find an exact match. If two equal objects return different hash codes, the second object will never be found because it lands in a different bucket. If two unequal objects return the same hash code, they end up in the same bucket, and the collection must compare them with Equals, which is slower but still correct.

This is why the contract between GetHashCode and Equals is strict: equal objects must have equal hash codes. The reverse is not required; unequal objects may share a hash code, but that increases collision rates and hurts performance.

The Contract Between GetHashCode and Equals

The .NET documentation defines three rules for GetHashCode:

  1. If two objects are equal, their GetHashCode values must be equal.
  2. GetHashCode must not change during the object's lifetime if the object is used as a key in a hash-based collection.
  3. GetHashCode should be fast and produce a good distribution to minimize collisions.

Rule 2 is often violated when the hash code is computed from mutable fields. If an object's hash code changes after it has been added to a Dictionary, the collection will not be able to locate it again. This is a common source of subtle bugs.

A Correct GetHashCode Override Example

Consider a simple Person class with Name and Age. If you want value equality, you override Equals and GetHashCode together.

public class Person { public string Name { get; } public int Age { get; } public Person(string name, int age) { Name = name; Age = age; } public override bool Equals(object? obj) { return obj is Person other && Name == other.Name && Age == other.Age; } public override int GetHashCode() { return HashCode.Combine(Name, Age); } }

The HashCode.Combine method is available in .NET Core 2.1 and later. It uses a randomized seed and combines fields in a way that produces a good distribution. If you are on an older framework, you can implement the same logic manually:

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, and the prime numbers help spread the values across the integer range. Both approaches are valid; HashCode.Combine is simpler and less error-prone.

Common Mistakes That Break Collections

A frequent mistake is using a mutable property in GetHashCode. If you change the property after the object is added to a Dictionary, the hash code changes and the object becomes unreachable. For example, a Person with a settable Age would break if Age changes after insertion.

Another mistake is overriding GetHashCode without overriding Equals. The default Equals uses reference equality, so two distinct objects with the same values will have different hash codes, which is fine, but the contract is broken if you override Equals to compare values and forget GetHashCode.

Returning a constant from GetHashCode is legal but causes every object to land in the same bucket, turning a Dictionary into a list and destroying performance. This is sometimes done to satisfy the compiler, but it is almost never the right choice.

Using a random number in GetHashCode is also wrong because the hash code must be consistent across calls within the same process.

Performance Considerations for Hash Codes

The hash code is called on every lookup, insertion, and removal. If it is expensive, it will dominate the cost of using the collection. For example, a hash code that iterates a large collection or performs string concatenation will be slow.

Collisions also hurt performance. When many objects share the same hash code, the collection must call Equals on each candidate, which is O(n) in the worst case. A good hash code spreads values evenly across the integer range.

For immutable objects, you can cache the hash code in a field to avoid recomputing it. This is a common optimization for types used frequently as keys.

private readonly int _hashCode; public Person(string name, int age) { Name = name; Age = age; _hashCode = HashCode.Combine(Name, Age); } public override int GetHashCode() => _hashCode;

This works only if the object is truly immutable. If any field used in the hash code can change, caching is unsafe.

When You Should Not Override GetHashCode

If you do not override Equals, you should not override GetHashCode either. The default implementations are consistent: they both use reference equality. If you only need reference identity, leave them alone.

If you never use the object as a key in a hash-based collection, overriding GetHashCode is unnecessary. It adds code and can introduce bugs if done incorrectly.

If you are using records, the compiler generates value-based Equals and GetHashCode for you. Similarly, value tuples and anonymous types have built-in implementations. Overriding them manually is redundant.

GetHashCode for Value Types and Records

For structs, the default GetHashCode uses reflection and can be slow. If you use a struct as a dictionary key and performance matters, you should override GetHashCode and Equals. The same rules apply: include all fields that participate in equality.

Records in C# 9 and later automatically generate GetHashCode and Equals based on the positional parameters or properties. This is convenient, but you should be aware that the generated hash code includes all the record's properties. If you want to exclude a property from equality, you must customize the record.

For example, a record with a mutable property that is not part of equality will still be included in the generated hash code if it is a primary constructor parameter. You can override the generated methods to change this behavior.

c# gethashcode: Practical Usage and Code Examples | RYUSLOG DEV