CodeOath
← All posts
C#65 min total · 17 parts

C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System

Contents — Part 5 of 17: Value Equality vs. Reference Equality
Part 5 of 17 · ~2 min

Value Equality vs. Reference Equality

== and .Equals() don't always mean the same thing, and which one applies depends on the type — a frequent source of subtle bugs.

// Value types: == compares the actual data
int x = 5, y = 5;
Console.WriteLine(x == y); // true

// Reference types (default, no overrides): == compares identity — same object?
class Point { public int X, Y; }
var p1 = new Point { X = 1, Y = 2 };
var p2 = new Point { X = 1, Y = 2 };
Console.WriteLine(p1 == p2);       // false — different objects, even with identical data
Console.WriteLine(p1.Equals(p2));  // also false — default Object.Equals is also identity-based

// string is the one built-in reference type where == is overloaded to compare value, not identity
string a = "hello";
string b = "hel" + "lo";
Console.WriteLine(a == b); // true — string overrides == to compare characters

Point above has no custom equality, so both == and the inherited Equals fall back to reference identity — two structurally identical Point objects are "not equal" unless you override that behavior yourself:

class Point2
{
    public int X, Y;
    public override bool Equals(object obj) =>
        obj is Point2 other && X == other.X && Y == other.Y;
    public override int GetHashCode() => HashCode.Combine(X, Y);
}

Overriding Equals without also overriding GetHashCode is a real, specific bug, not just a style nitpick: hash-based collections (Dictionary, HashSet) use GetHashCode to find the right bucket before calling Equals at all. Two objects that are Equals-equal but report different hash codes can end up in different buckets and never be found as equal by a lookup — silently breaking Dictionary and HashSet behavior. The compiler doesn't enforce pairing them; it's a contract you have to uphold yourself (or get for free — see records, next).