14.2. Record Types#

You have been writing record since Chapter 9 as a shorthand for simple data types. Here we look at what record actually provides and when to prefer it over a class.

14.2.1. What record Gives You#

A record is a reference type (like a class) with four things added automatically:

  1. Value-based equality — two records are equal if all their properties are equal

  2. ToString() — prints all property values readably

  3. Deconstruction — unpack into variables like a tuple

  4. with expressions — create a copy with selected properties changed

14.2.1.1. Value-Based Equality#

record Point(int X, int Y);

var a = new Point(1, 2);
var b = new Point(1, 2);
var c = new Point(3, 4);

Console.WriteLine(a == b);   // True  — same values
Console.WriteLine(a == c);   // False
Console.WriteLine(a);        // Point { X = 1, Y = 2 }

14.2.1.2. with Expressions#

Create a modified copy without mutating the original.

record Person(string Name, int Age);

var alice = new Person("Alice", 30);
var olderAlice = alice with { Age = 31 };

Console.WriteLine(alice);       // Person { Name = Alice, Age = 30 }
Console.WriteLine(olderAlice);  // Person { Name = Alice, Age = 31 }
Console.WriteLine(alice == olderAlice);  // False

14.2.1.3. Deconstruction#

record Point(int X, int Y);

var p = new Point(3, 7);
var (x, y) = p;
Console.WriteLine($"x={x}, y={y}");  // x=3, y=7

14.2.2. Positional vs. Standard Records#

// Positional — compact; properties are init-only
record Point(int X, int Y);

// Standard — more control over property visibility/mutability
record Person
{
    public string Name { get; init; } = "";
    public int    Age  { get; set;  }
}

Use positional for immutable value-like data (coordinates, domain events, DTO return values). Use standard when some properties need to be mutable.

14.2.3. Init-Only Properties#

init is like set, but only callable during object construction. It makes individual properties settable at creation time while remaining immutable afterward — without needing a positional record.

record Person
{
    public string Name { get; init; } = "";
    public int    Age  { get; init; }
}

This is what the compiler generates automatically for positional record properties.

record Person
{
    public string Name { get; init; } = "";
    public int    Age  { get; init; }
}

var p = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(p);  // Person { Name = Alice, Age = 30 }

// p.Age = 31;  // compile error — init-only after construction

// Works fine in object initializer:
var q = p with { Age = 31 };  // creates a copy
Console.WriteLine(q);  // Person { Name = Alice, Age = 31 }

Init-only properties are also common in regular classes when you want immutable-after-creation semantics without making the whole type a record:

class Config
{
    public string Host { get; init; } = "localhost";
    public int    Port { get; init; } = 8080;
}

14.2.4. record struct#

record struct gives the same conveniences on a value type — allocated on the stack, copied on assignment.

record struct Size(int Width, int Height);

Prefer record struct for tiny, frequently-created data (e.g., 2D coordinates in a game loop). Prefer record (class) for everything else.

14.2.5. record vs. class vs. struct#

class

record

struct / record struct

Equality

Reference

Value

Value

with expression

Auto ToString

Heap allocated

Inheritance

Use record when your type is primarily data — no complex behaviour, identity does not matter, immutability is preferred.

14.2.6. Practice#

  1. Define record Temperature(double Celsius) and add a computed property Fahrenheit. Print two equal temperatures and verify == is true.

  2. Using with, create a sequence: start with Person("Bob", 20), then produce versions at age 21, 22, 23 without mutation.

  3. Define record struct Vector2(float X, float Y) and write an Add method that returns a new Vector2.

Footnotes