== 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).