Equality: Equals, GetHashCode & IEquatable
10 questions foundFor a reference type without overrides, both == and the inherited Object.Equals() check REFERENCE equality — whether two variables point to the exact SAME object in memory — not whether their content/data is logically equivalent.
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2); // False -- different objects, same data
Console.WriteLine(p1.Equals(p2)); // False -- same, unless Equals is overridden
Real-world example Understanding why two 'identical-looking' objects don't compare as equal until you explicitly define value equality.
Common follow-ups: How does this default behavior differ for value types like struct?
Value vs Reference Types
System.ValueType (the base for all structs) overrides Equals() to perform FIELD-BY-FIELD value comparison by default (via reflection, which is slower than a hand-written override), unlike a plain class which defaults to reference equality — so two structs with identical field values ARE considered equal out of the box.
public struct Point { public int X, Y; }
var p1 = new Point { X = 1, Y = 2 };
var p2 = new Point { X = 1, Y = 2 };
Console.WriteLine(p1.Equals(p2)); // True -- structs compare by value by default
Real-world example Relying on a simple struct's built-in value equality for basic data-holder types without writing custom Equals() logic.
Common follow-ups: Why is the default reflection-based struct Equals() considered relatively slow, and how would you speed it up?
Structs Boxing & Unboxing
.NET's contract requires that if two objects are Equal() (return true), they MUST also return the SAME GetHashCode() value — hash-based collections like Dictionary and HashSet rely on this to correctly bucket and find equal objects; overriding only one breaks that contract and causes subtle collection bugs.
public class Point {
public int X, Y;
public override bool Equals(object? obj) =>
obj is Point other && X == other.X && Y == other.Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
}
Real-world example Correctly making a custom class usable as a Dictionary key or HashSet element based on its logical, value-based identity.
Common follow-ups: What specifically goes wrong in a Dictionary or HashSet if you override Equals() but forget GetHashCode()?
Collections
IEquatable<T>.Equals(T other) provides a STRONGLY-TYPED equality method that avoids the boxing (for structs) and the type-checking/casting overhead that Equals(object) requires — many generic collections and LINQ methods automatically prefer the IEquatable<T> overload when available, giving better performance.
public struct Point : IEquatable<Point> {
public int X, Y;
public bool Equals(Point other) => X == other.X && Y == other.Y; // strongly-typed, no boxing
public override bool Equals(object? obj) => obj is Point p && Equals(p); // delegates to the typed version
public override int GetHashCode() => HashCode.Combine(X, Y);
}
Real-world example Implementing IEquatable<T> on a value type used heavily in a List<T>.Contains() or Dictionary key scenario for better performance.
Common follow-ups: Why does implementing IEquatable<T> matter MORE for structs than for classes, performance-wise?
Structs Boxing & Unboxing
HashCode.Combine() takes multiple field values and produces a well-mixed, evenly-distributed hash code using a proven algorithm — far better than naive approaches like simply XOR-ing fields together (which can produce poor distribution or even zero for identical field pairs), and is the modern recommended standard approach.
public override int GetHashCode() => HashCode.Combine(FirstName, LastName, BirthDate);
// vs. the old, worse approach:
// public override int GetHashCode() => FirstName.GetHashCode() ^ LastName.GetHashCode(); // poor distribution
Real-world example Writing a correct, well-distributed GetHashCode() override for a multi-field value object used as a Dictionary key.
Common follow-ups: What happens with HashCode.Combine() if you need to combine MORE than 8 values, given its overloads are limited?
Collections
If an object's hash code can CHANGE after it's inserted into a hash-based collection (Dictionary, HashSet) because it depends on a mutable field, the collection can no longer find the object in its correct bucket — the object effectively becomes 'lost' in the collection, even though .Contains() might still show it exists if you enumerate manually.
public class MutablePoint {
public int X; // mutable field used in hashing -- dangerous!
public override int GetHashCode() => X.GetHashCode();
}
var set = new HashSet<MutablePoint>();
var p = new MutablePoint { X = 1 };
set.Add(p);
p.X = 99; // mutated after insertion!
Console.WriteLine(set.Contains(p)); // False! -- 'lost' in the wrong bucket now
Real-world example Debugging a mysterious bug where an object provably added to a HashSet or Dictionary key can no longer be found after some of its fields were later mutated.
Common follow-ups: Why do many style guides recommend using immutable types (or records) specifically for Dictionary/HashSet keys?
Records & Pattern Matching
Unlike a regular class (reference equality by default), a record automatically generates VALUE-based Equals(), GetHashCode(), and == operator overloads based on ALL its properties — two record instances with identical property values are considered equal, without you writing any equality code yourself.
public record Point(int X, int Y);
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2); // True -- value equality, auto-generated
Console.WriteLine(p1.Equals(p2)); // True
Real-world example Relying on a record's automatically-generated value equality for a DTO or value object without writing manual Equals()/GetHashCode() overrides.
Common follow-ups: How would you override just ONE property's contribution to a record's auto-generated equality, if needed?
Records & Pattern Matching
A careful Equals() implementation for an inheritance hierarchy typically checks `GetType() == other.GetType()` (not just 'is BaseType') to prevent a base and derived instance with otherwise-matching fields from incorrectly comparing as equal — since a derived type may add fields that the base's Equals() logic isn't aware of, treating them as equal could violate the type's actual semantic identity.
public class Shape {
public override bool Equals(object? obj) =>
obj != null && GetType() == obj.GetType() && EqualsCore((Shape)obj); // exact type match required
protected virtual bool EqualsCore(Shape other) => true;
}
public class Circle : Shape {
public double Radius;
protected override bool EqualsCore(Shape other) => other is Circle c && Radius == c.Radius;
}
Real-world example Correctly implementing equality across a class hierarchy where a base Shape and a derived Circle should never accidentally compare as equal.
Common follow-ups: What subtle bug can occur if you use 'is BaseType' instead of an exact GetType() check in this scenario?
Interfaces & Abstract Classes
IComparable<T>.CompareTo() should return 0 EXACTLY when IEquatable<T>.Equals() returns true, maintaining a consistent notion of equality between the two — a class that says two objects are 'equal' via Equals() but 'not equal' (non-zero CompareTo) via ordering has an internally inconsistent, buggy identity model that can cause sorting and collection bugs.
public class Money : IEquatable<Money>, IComparable<Money> {
public decimal Amount;
public bool Equals(Money? other) => other != null && Amount == other.Amount;
public int CompareTo(Money? other) => Amount.CompareTo(other?.Amount ?? 0);
// Equals and CompareTo agree: equal amounts -> Equals true AND CompareTo returns 0
}
Real-world example Implementing a value type (like Money or a version number) that needs both correct equality checks and correct sort ordering, consistently.
Common follow-ups: What real bug can occur in a SortedSet<T> or SortedDictionary<TKey,TValue> if CompareTo() and Equals() disagree?
Generics
List<T> (and most standard collections) inherit REFERENCE equality from Object by default — two lists with identical elements are NOT considered equal via Equals() unless you use a dedicated comparison method like Enumerable.SequenceEqual(), which explicitly walks both sequences and compares corresponding elements pairwise.
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3 };
Console.WriteLine(list1.Equals(list2)); // False -- reference equality
Console.WriteLine(list1.SequenceEqual(list2)); // True -- explicit element-by-element comparison
Real-world example Correctly comparing two lists of parsed data for content equality in a unit test assertion, rather than accidentally comparing references.
Common follow-ups: Does SequenceEqual() also check that both sequences are the SAME LENGTH before comparing elements?
LINQ