Skip to content
Records and Pattern Matching
step 1/6

Reading — step 1 of 6

Learn

~3 min readModern C#

C# 9 added records for concise immutable types, and C# 8-10 expanded pattern matching into a full language feature. Together they enable a functional style of data modeling.

Records

public record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);          // True (structural equality)
Console.WriteLine(p1);                 // Point { X = 3, Y = 4 }

var moved = p1 with { X = 10 };       // copy with one property changed

A single line replaces 30+ lines of equivalent immutable class boilerplate. Records auto-generate:

  • Constructor matching the parameters
  • Properties (init-only, can't be reassigned)
  • Equals, GetHashCode, ToString
  • A Deconstruct method for destructuring
  • The with expression for copy-and-modify

Record class vs record struct (C# 10+)

public record class User(string Name, int Age);    // reference type (default)
public record struct Vec3(float X, float Y, float Z);   // value type

record struct is great for small immutable data that doesn't need heap allocation.

Pattern matching with switch expressions

string Describe(object o) => o switch {
    null => "nothing",
    int n when n < 0 => $"negative {n}",
    int n => $"positive {n}",
    string s when s.Length > 10 => $"long string",
    string s => $"short: {s}",
    Point(0, 0) => "origin",
    Point(var x, var y) => $"({x}, {y})",
    _ => "unknown"
};

Patterns:

  • Type patternint n (matches if cast succeeds, binds to n)
  • Constant pattern0, null, "hi"
  • Property pattern{ Length: > 10 }, { X: 0, Y: 0 }
  • Positional patternPoint(var x, var y) (uses Deconstruct)
  • Var patternvar anything (always matches, binds)
  • Discard_
  • Logical patterns (C# 9+) — not null, > 0 and < 100, "yes" or "y"

Property patterns

bool IsAdult(Person p) => p is { Age: >= 18 };
bool IsAdminEditor(User u) => u is { Role: "admin" or "editor" };

Switch expression with records

public record Shape;
public record Circle(double Radius) : Shape;
public record Square(double Side) : Shape;
public record Rect(double Width, double Height) : Shape;

double Area(Shape s) => s switch {
    Circle(var r) => Math.PI * r * r,
    Square(var side) => side * side,
    Rect(var w, var h) => w * h,
};

Note: when the switch is exhaustive, the compiler doesn't require a default. With non-sealed hierarchies it warns about non-exhaustive matches.

On this platform's grader (C# 7)

Records and switch expressions are C# 8/9 features — our grader runs Mono's compiler, which doesn't accept them (it supports is type patterns, but not pattern switch). The graded exercise therefore uses the is pattern, which expresses the same dispatch:

static double Area(Shape s)
{
    if (s is Circle c)  return Math.PI * c.Radius * c.Radius;
    if (s is Square sq) return sq.Side * sq.Side;
    return 0;
}

Same pattern-matching idea — s is Circle c tests the runtime type AND binds c in one step. The switch-expression form you learned above is what you'll write in modern codebases.

Common mistakes

  • Trying to mutate a record property — they're init-only. Use with to create a modified copy.
  • Thinking record class == means reference equality — records OVERRIDE == for structural equality. Different from regular classes.
  • Pattern-matching on records without using Deconstructr is Point p && p.X == 0 works but r is Point(0, var y) is cleaner.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…