Equality: Equals, GetHashCode & IEquatable

10 questions found

What is the difference between reference equality (==) and value equality (Equals()) for a reference type by default?

Beginner
For 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

How does a struct's default Equals() behavior differ from a class's, even without any overrides?

Beginner
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

How do you correctly override Equals() and GetHashCode() together on a class, and why must they always be overridden as a pair?

Intermediate
.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

How does implementing IEquatable<T> improve on just overriding Equals(object), and why is it recommended alongside the override?

Intermediate
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

How does HashCode.Combine() help you write a correct and well-distributed GetHashCode() implementation?

Intermediate
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

Why is it critical that GetHashCode() only depend on IMMUTABLE fields, and what bug occurs if you violate this rule?

Advanced
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

How does record type equality work by default, and how does it differ fundamentally from a regular class's default equality?

Advanced
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

How should Equals() be implemented differently for a class designed to support INHERITANCE, to correctly handle equality between a base type and derived types?

Advanced
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

How do you correctly implement IComparable<T> alongside IEquatable<T>, and what consistency rule must they both satisfy together?

Advanced
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

How does structural (deep) equality for collections work, and why doesn't List<T>.Equals() perform element-by-element comparison by default?

Advanced
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