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

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

Contents — Part 7 of 17: Records: Value-Like Semantics Without Giving Up Reference Types
Part 7 of 17 · ~2 min

Records: Value-Like Semantics Without Giving Up Reference Types

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:

classrecordstructrecord struct
Value or reference typeReferenceReferenceValueValue
Default ==/EqualsIdentityValue (auto-generated)Value (via ValueType.Equals, uses reflection unless overridden)Value (auto-generated, no reflection)
Copy semanticsShares on assignmentShares on assignmentCopies on assignmentCopies on assignment
Built-in with non-destructive mutationNoYesNoYes

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.