Introduction: That One Mistake That Slows Down Your Code
\n\nAnyone who has ever written in C# and encountered Dictionary or HashSet has eventually stumbled upon this question on Stack Overflow. Thousands of views, dozens of answers, but the essence is the same: if you overrode Equals in your class but forgot about GetHashCode — your code may work incorrectly or be terribly slow. Why? Let's figure it out with real examples.
Imagine you are writing an airline ticket booking system. You have a Flight class with a unique flight number and date. You want to use this class as a key in a Dictionary. It seems simple. But without a proper GetHashCode, your dictionary can degenerate into a list with O(n) search speed instead of O(1).
The Magic of Hash Tables: How Dictionary Works in C#
\n\nTo understand the problem, you need to look under the hood of Dictionary. Internally, it is an array of "buckets". When you add an element, C# calls the GetHashCode() method on the key to determine the bucket number. If the bucket already has elements, Equals() is called to compare with each of them. It is this tandem that provides lightning-fast lookup.
The Contract That Must Not Be Broken
\nIn C#, there is an unwritten but strict contract:
\n- \n
- If
Equals()returnstruefor two objects, their hash codes must be equal. \n - If the hash codes are different,
Equals()will never be called (this is an optimization). \n - The reverse is not required: different objects can have the same hash code (collision). \n
When you override only Equals but not GetHashCode, you inherit the implementation from System.Object, which is based on the memory reference. Two objects that are "equal" by your logic will have different hash codes. And that's where the trouble begins.
Example of the Problem: Flights Lost in the Dictionary
\n\nTake a look at this code. It seems like it should work, but it doesn't:
\n\npublic class Flight\n{\n public string FlightNumber { get; }\n public DateTime Date { get; }\n\n public Flight(string flightNumber, DateTime date)\n {\n FlightNumber = flightNumber;\n Date = date;\n }\n\n public override bool Equals(object obj)\n {\n if (obj is Flight other)\n return FlightNumber == other.FlightNumber && Date == other.Date;\n return false;\n }\n \n // GetHashCode is NOT overridden!\n}\n\n// Usage\nvar flights = new Dictionary<Flight, string>();\nvar f1 = new Flight("SU100", new DateTime(2024, 12, 25));\nvar f2 = new Flight("SU100", new DateTime(2024, 12, 25));\n\nflights[f1] = "Moscow - Paris";\n\n// Error! f2 is not found, even though f1.Equals(f2) == true\nConsole.WriteLine(flights.ContainsKey(f2)); // False\n\nWhy is this? f1 and f2 are different objects in memory. They have different hash codes from Object.GetHashCode(). The dictionary places f1 in bucket #123, but looks for f2 in bucket #456. Equals is never even called! The result — the element is not found.
The Performance Problem: When Everything Works, But Slowly
\n\nWorst of all is a partial solution. Suppose you overrode GetHashCode, but did it poorly, for example, by returning a constant:
public override int GetHashCode() => 42; // All objects in one bucket\n\nFormally, the contract is satisfied (equal objects have the same hash). But performance will plummet: all objects will end up in one bucket, and the Dictionary will degenerate into a list. Insertion and lookup become O(n). For a dictionary with 10,000 elements, this is a disaster.