A record (introduced in C# 9) is still a reference type by default, but the compiler automatically generates value-based Equals, GetHashCode, and ToString for it — solving exactly the pairing problem from the equality section above, without writing it by hand:
record Point3(int X, int Y);
var p1 = new Point3(1, 2);
var p2 = new Point3(1, 2);
Console.WriteLine(p1 == p2); // true — records compare by value automatically
Console.WriteLine(p1.Equals(p2)); // true
Console.WriteLine(p1); // "Point3 { X = 1, Y = 2 }" — auto-generated ToString
Records also come with built-in non-destructive mutation via with, which returns a new record with specified properties changed, leaving the original untouched — a natural fit for immutable data:
var p3 = p1 with { Y = 99 };
Console.WriteLine(p1); // Point3 { X = 1, Y = 2 } — unchanged
Console.WriteLine(p3); // Point3 { X = 1, Y = 99 }
C# also supports record struct — a value type with the same auto-generated value equality a record gets, if you want struct copy semantics and don't want to hand-write Equals/GetHashCode yourself:
class | record | struct | record struct | |
|---|---|---|---|---|
| Value or reference type | Reference | Reference | Value | Value |
Default ==/Equals | Identity | Value (auto-generated) | Value (via ValueType.Equals, uses reflection unless overridden) | Value (auto-generated, no reflection) |
| Copy semantics | Shares on assignment | Shares on assignment | Copies on assignment | Copies on assignment |
Built-in with non-destructive mutation | No | Yes | No | Yes |
One easy-to-miss detail: a plain struct's inherited Equals (from ValueType) does compare field values, not identity — but it does so via reflection unless you override it, which is considerably slower than a hand-written or record-generated comparison. This is a common performance footgun in hot paths that rely on default struct equality without realizing the cost.