C# DISCUSSION

Why must I override GetHashCode when I override Equals in a C# class?

Started by kettie GetHashCodeEquals overrideDictionary keyshash collisionsvalue equality
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

Why must I override GetHashCode when I override Equals in a C# class?

kettie C# Forum
#1

I have a class PinId with Port and Number properties, and I overrode Equals so that two instances with the same port and number compare equal. The compiler warns (CS0659) that I override Equals but not GetHashCode. I ignored it, and now dictionary.ContainsKey(new PinId("A", 5)) returns false even though an equal key was added earlier, while list.Contains with the same object returns true.

Why does the dictionary not find the key, and what does a correct GetHashCode look like?

Community replies 5

Re: Why must I override GetHashCode when I override Equals in a C# class?

#2

Dictionary and HashSet do not compare the lookup key against every stored key. They call GetHashCode(), use the number to select a bucket, and call Equals only on entries in that bucket with a matching hash. The default GetHashCode of a class is based on object identity, so two separate PinId("A", 5) instances almost certainly produce different numbers, land in different buckets, and Equals is never called. List.Contains scans linearly using Equals alone, which is why it works.

The contract: if a.Equals(b) is true, a.GetHashCode() must equal b.GetHashCode(). The reverse is not required; unequal objects may share a hash, and that only costs speed.

Re: Why must I override GetHashCode when I override Equals in a C# class?

#3

Combine the same fields that Equals compares: public override int GetHashCode() => HashCode.Combine(Port, Number);. HashCode.Combine is part of .NET Core 2.1 and later; on .NET Framework it is available through a NuGet package.

The classic manual version is unchecked { int h = 17; h = h * 31 + (Port?.GetHashCode() ?? 0); h = h * 31 + Number; return h; }. The unchecked block lets the arithmetic wrap instead of throwing when overflow checking is enabled. Avoid simply XOR-ing the fields: a ^ b gives the same hash for (3, 5) and (5, 3), and zero whenever both values are equal, which produces many collisions.

Re: Why must I override GetHashCode when I override Equals in a C# class?

#4

The hash must not change while the object is used as a key. If Port or Number has a setter and you modify a key that is already in a dictionary, its stored bucket no longer matches its new hash. The entry becomes unfindable, although it is still in the collection and shows up when you enumerate it.

So make key types immutable: get-only properties assigned in the constructor. Also implement IEquatable<PinId> with a typed Equals(PinId other) and have Equals(object) forward to it, and if you overload == and !=, keep them consistent with Equals.

Re: Why must I override GetHashCode when I override Equals in a C# class?

#5

On current C# versions, let the compiler write all of this. public record PinId(string Port, int Number); (C# 9) generates value-based Equals, GetHashCode, ==, != and ToString over the declared properties, which are init-only, so the mutation problem disappears as well. record struct (C# 10) is the value-type version.

For ad-hoc composite keys a tuple works directly: Dictionary<(string Port, int Number), Pin>, since value tuples implement equality and hashing over their elements. Plain structs get field-by-field equality by default, but that default implementation can fall back to reflection and be slow, so override both methods or use a record struct.

Re: Why must I override GetHashCode when I override Equals in a C# class?

#6

Two things not to do with hash codes. Do not persist them or send them to another process: string.GetHashCode() is randomised per process on .NET Core and later, so the same string gives a different value on each run. For a stable identifier use a real checksum or hash, such as CRC32 or SHA-256, over defined bytes. And do not treat equal hashes as equality; an int has only 2^32 values, so collisions are unavoidable for larger key spaces.

A quick check of your fix: create two equal instances, confirm that Equals is true and the hash codes match, then add both to a HashSet<PinId> and verify that Count is 1. If you cannot change the key type, pass an IEqualityComparer<T> to the dictionary constructor instead.

TEP COMMUNITY