Table of Contents#
- What is
Object.GetHashCode()? - How
GetHashCode()Works: The Contract - Default Implementation
- 3.1 Reference Types
- 3.2 Value Types
- Overriding
GetHashCode() - Best Practices for Overriding
- Common Pitfalls
- Example Usage
- When to Use
GetHashCode() - Conclusion
- References
What is Object.GetHashCode()?#
The GetHashCode() method is defined in System.Object and returns an int (32-bit signed integer) called a hash code. A hash code is a numeric representation of an object’s state, designed to facilitate fast grouping and retrieval in hash-based data structures.
Key purposes of GetHashCode():
- Quick Comparison: Hash codes allow collections to group objects into "buckets," reducing the number of equality checks needed to find an object.
- Efficient Lookups: In hash tables, objects with the same hash code are stored in the same bucket. This makes lookups O(1) on average (vs. O(n) for linear search).
How GetHashCode() Works: The Contract#
The behavior of GetHashCode() is governed by a critical contract defined in the .NET documentation. Violating this contract can lead to unexpected behavior in hash-based collections. The contract states:
-
Consistency: For the same object,
GetHashCode()must return the same value if none of the fields used in theEquals()method have changed. This ensures stability during the object’s lifetime. -
Equality Implies Equal Hash Codes: If two objects are equal (i.e.,
obj1.Equals(obj2)returnstrue), their hash codes must be equal. -
Unequal Hash Codes Imply Inequality: If two objects have different hash codes, they are guaranteed to be unequal. However, the reverse is not true: equal hash codes do not guarantee equality (this is called a "hash collision").
Default Implementation#
The default GetHashCode() implementation varies between reference types and value types.
3.1 Reference Types#
For reference types (e.g., classes), the default GetHashCode() returns a value based on the object’s identity (not its content). This means two distinct instances with identical data will have different hash codes.
Example:
public class Person {
public string Name { get; set; }
public int Age { get; set; }
}
var alice1 = new Person { Name = "Alice", Age = 30 };
var alice2 = new Person { Name = "Alice", Age = 30 };
// Default GetHashCode() uses object identity: different instances → different hash codes
Console.WriteLine(alice1.GetHashCode() == alice2.GetHashCode()); // Output: False3.2 Value Types#
For value types (e.g., structs, int, string), the default GetHashCode() computes a hash code based on the values of the type’s fields. For example:
- For primitive types like
int, the hash code is the value itself. - For custom structs, the default implementation combines the hash codes of all fields (e.g., via XOR), but this can lead to collisions.
Example:
public struct Point {
public int X { get; set; }
public int Y { get; set; }
}
var point1 = new Point { X = 1, Y = 2 };
var point2 = new Point { X = 1, Y = 2 };
// Value type: same field values → same hash code
Console.WriteLine(point1.GetHashCode() == point2.GetHashCode()); // Output: TrueOverriding GetHashCode()#
You must override GetHashCode() if you override Equals(). This ensures the contract is maintained: equal objects have equal hash codes.
Why Override?#
- Semantic Equality: If your type defines "value-based equality" (e.g., two
Personobjects are equal if theirNameandAgematch), the default reference-based hash code is incorrect. - Hash Collection Compatibility: Hash-based collections (e.g.,
Dictionary) rely on consistent hash codes for correct behavior.
Best Practices for Overriding#
To implement GetHashCode() correctly, follow these best practices:
1. Use Immutable Fields#
Hash codes should depend only on immutable fields (fields that don’t change after object construction). If a mutable field is used, modifying it will change the hash code, breaking collections that store the object.
2. Combine Hash Codes Carefully#
To minimize collisions, combine hash codes of multiple fields using a formula that spreads values evenly. A common approach is to multiply by a prime number (e.g., 31) and add subsequent hash codes:
public override int GetHashCode() {
unchecked { // Allow integer overflow (harmless for hash codes)
int hash = 17; // Initial prime seed
hash = hash * 31 + Name.GetHashCode(); // 31 is a prime
hash = hash * 31 + Age;
return hash;
}
}- Why 31? It’s an odd prime, and
31 * ican be computed as(i << 5) - i, which is efficient for CPUs.
3. Use HashCode.Combine (Modern .NET)#
In .NET Core 2.1+ and .NET 5+, use System.HashCode.Combine() for a concise, optimized way to combine hash codes:
public override int GetHashCode() => HashCode.Combine(Name, Age);4. Avoid Constant Hash Codes#
Never return a constant (e.g., return 0). This forces all objects into a single bucket, degrading collection performance to O(n).
5. Document Hash Code Stability#
If your type is mutable, document that it should not be used as a key in hash-based collections (since hash codes can change).
Common Pitfalls#
1. Forgetting to Override GetHashCode() When Overriding Equals()#
If Equals() is overridden but GetHashCode() is not, equal objects may have different hash codes, breaking hash collections.
2. Using Mutable Fields#
Modifying a field used in GetHashCode() after adding the object to a HashSet or Dictionary will make it unfindable, as the hash code changes.
3. Poor Hash Code Distribution#
Using simple XOR (hash = a ^ b) for combining fields can cause collisions (e.g., (a=1, b=2) and (a=2, b=1) have the same hash code).
4. Relying on Hash Code Uniqueness#
Hash codes are not unique! Two different objects can have the same hash code (collision). Always use Equals() to confirm equality.
Example Usage#
Example 1: Default Behavior#
public class Car {
public string Model { get; set; }
public int Year { get; set; }
}
var car1 = new Car { Model = "Tesla", Year = 2023 };
var car2 = new Car { Model = "Tesla", Year = 2023 };
// Default GetHashCode() uses reference identity → different hash codes
Console.WriteLine(car1.GetHashCode() == car2.GetHashCode()); // Output: False
Console.WriteLine(car1.Equals(car2)); // Output: False (default Equals checks reference)Example 2: Overriding Equals() and GetHashCode()#
public class Car {
public string Model { get; } // Immutable field
public int Year { get; } // Immutable field
public Car(string model, int year) {
Model = model;
Year = year;
}
public override bool Equals(object obj) {
if (obj is not Car other) return false;
return Model == other.Model && Year == other.Year;
}
public override int GetHashCode() {
// Use HashCode.Combine for simplicity and efficiency
return HashCode.Combine(Model, Year);
}
}
var car1 = new Car("Tesla", 2023);
var car2 = new Car("Tesla", 2023);
Console.WriteLine(car1.Equals(car2)); // Output: True
Console.WriteLine(car1.GetHashCode() == car2.GetHashCode()); // Output: TrueExample 3: Mutable Objects and Hash Code Instability#
public class MutablePerson {
public string Name { get; set; } // Mutable field
public int Age { get; set; } // Mutable field
public override bool Equals(object obj) {
if (obj is not MutablePerson other) return false;
return Name == other.Name && Age == other.Age;
}
public override int GetHashCode() => HashCode.Combine(Name, Age);
}
var hashSet = new HashSet<MutablePerson>();
var person = new MutablePerson { Name = "Bob", Age = 25 };
hashSet.Add(person);
// Modify a field used in GetHashCode()
person.Age = 26;
// Now, hashSet.Contains(person) may return false!
Console.WriteLine(hashSet.Contains(person)); // Output: False (unexpected behavior)When to Use GetHashCode()#
GetHashCode() is primarily used implicitly by hash-based collections like:
Dictionary<TKey, TValue>: Uses keys’ hash codes to group entries.HashSet<T>: Uses hash codes to avoid duplicate entries.ConcurrentDictionary<TKey, TValue>: Thread-safe hash table.
You rarely call GetHashCode() directly unless implementing custom hash-based logic.
Conclusion#
Object.GetHashCode() is a cornerstone of efficient collection operations in C#. By understanding its contract, default behavior, and best practices for overriding, you can ensure correct and performant hash-based collections. Always override GetHashCode() when overriding Equals(), use immutable fields, and avoid common pitfalls like mutable hash codes.