Skip to content
Pattern Matching and switch Expressions
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, LINQ Internals, Pattern Matching

Modern C# (7.0 onward, with major additions in 8/9/10/11) has rich pattern matching — type tests, property patterns, positional patterns, switch expressions. The Microsoft docs cover this thoroughly; modern codebases lean on it heavily.

Type pattern (C# 7+)

object obj = "hello";
if (obj is string s)
{
    Console.WriteLine(s.Length);     // s is bound, narrowed to string
}

Replaces the verbose obj is string && (string)obj. Bind once, use many times.

switch statements with patterns

static string Describe(object o) => o switch
{
    null => "null",
    int n when n < 0 => "negative int",
    int 0 => "zero",
    int => "positive int",
    string { Length: 0 } => "empty string",
    string s => $"string of length {s.Length}",
    _ => "something else"
};

Switch EXPRESSION (returns a value) vs switch STATEMENT (no value). Prefer expressions where possible.

Property patterns

record Point(int X, int Y);

static string Quadrant(Point p) => p switch
{
    { X: 0, Y: 0 } => "origin",
    { X: 0 } => "on Y axis",
    { Y: 0 } => "on X axis",
    { X: > 0, Y: > 0 } => "first quadrant",
    { X: < 0, Y: > 0 } => "second quadrant",
    { X: < 0, Y: < 0 } => "third quadrant",
    _ => "fourth quadrant"
};

Match on properties without writing the type name. { X: 0 } matches any object with X == 0.

C# 9 added relational patterns (> 0, < 0) and logical patterns (and, or, not):

static string Grade(int score) => score switch
{
    >= 90 and <= 100 => "A",
    >= 80 and < 90 => "B",
    >= 70 and < 80 => "C",
    >= 60 and < 70 => "D",
    < 60 and >= 0 => "F",
    _ => "invalid"
};

Positional patterns (with records)

static string Describe(Point p) => p switch
{
    (0, 0) => "origin",
    (var x, var y) when x == y => "on diagonal",
    (var x, _) when x > 0 => "right side",
    _ => "other"
};

For types with Deconstruct (records have it auto-generated), positional pattern matching destructures.

List patterns (C# 11+)

static string DescribeArray(int[] arr) => arr switch
{
    [] => "empty",
    [var x] => $"single element {x}",
    [var first, .. var rest, var last] => $"first {first}, last {last}, {rest.Length} in middle",
    _ => "complicated"
};

[..] matches the rest. Powerful for list/array decomposition.

records — value-equality classes

public record User(string Name, int Age);

var a = new User("Ada", 36);
var b = new User("Ada", 36);

a == b              // true — records compare by value
a.ToString();       // "User { Name = Ada, Age = 36 }"
var older = a with { Age = 37 };    // non-destructive update

record (C# 9+) auto-generates Equals, GetHashCode, ToString, Deconstruct, with expression. Like Kotlin data class or Swift's Equatable structs.

record struct (C# 10+) — value-type record. Best of both: cheap copy AND value semantics.

Exhaustiveness in switch expressions

C# warns (but doesn't error) when a switch expression isn't exhaustive. For sealed hierarchies or enums, it figures out the cases:

public enum Color { Red, Green, Blue }

static string ToHex(Color c) => c switch
{
    Color.Red => "#ff0000",
    Color.Green => "#00ff00",
    Color.Blue => "#0000ff"
    // No default — compiler warns about exhaustiveness
};

Add an enum value → compiler warns the switch is incomplete.

Common mistakes

  • switch STATEMENT vs EXPRESSION — different syntax. Statement uses case X: and break;. Expression uses X => ... with comma separators.
  • Forgetting the discard _ — pattern that matches anything is _, not default:.
  • Property pattern on null{ X: 0 } doesn't match null. Add explicit null => ....
  • Records mutability — records are immutable by default. Use with for updates.
  • Positional pattern without Deconstruct — only works on records and types you've defined Deconstruct on.

Discussion

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

Sign in to post a comment or reply.

Loading…