dotnet/C# Language
Pattern matching
basicsTL;DR
// Switch expression + patterns: modern C# control flow.
static string Describe(Shape shape) => shape switch
{
Circle { Radius: 0 } => "a point",
Circle c when c.Radius > 100 => "a huge circle",
Circle => "a circle",
Rectangle { W: var w, H: var h } when w == h => "a square",
null => "nothing",
_ => "something else",
};
// 'is' patterns introduce scoped variables, combine with and/or/not.
if (response is { StatusCode: >= 200 and < 300, Body.Length: > 0 })
Process(response.Body);
// List patterns (C# 11+).
var summary = numbers switch
{
[] => "empty",
[var only] => $"just {only}",
[var first, .., var last] => $"{first} … {last}",
};
How it works
C# has one pattern grammar shared by is, switch statements, switch
expressions and when guards. Knowing the catalogue means every context
uses the same vocabulary:
- Constant —
case 0,case "GET",case null— compares with==or reference equality. - Declaration and type —
is Circle cbinds a typed local only when the match succeeds; a bareCirclepattern checks the type without binding. - Property —
{ Radius: 0 }recurses into members, nesting as deep as the object graph allows, including nested member access likeBody.Length. - Positional —
Circle(var radius)destructures throughDeconstruct, the same method tuple deconstruction (var (x, y) = point;) relies on. - Relational —
> 100,>= 200 and < 300— compares with<,<=,>,>=against a constant. - Logical combinators —
and,or,notcompose any of the above, including chaining relational patterns into a range check. var— always matches, binds the value under any name; useful to capture a sub-expression for awhenclause.- Discard —
_always matches and binds nothing; the conventional catch-all arm. - List and slice —
[],[var first, .., var last]— matches array or list length and shape, with..as a slice for “everything in between.”
A switch statement and a switch expression compile the same
patterns but differ in shape: the statement is a series of case blocks,
each exited explicitly with break, return, or goto case, with an
optional default: catch-all, while the expression is a single value
produced from an exhaustive set of arms — closer to a function than a
control-flow construct. Reach for the expression whenever every arm just
produces a value.
Under the hood, the compiler doesn’t test each arm top to bottom like a
naive if/else if chain. It builds a decision DAG: type checks and
constant comparisons that appear in more than one arm are evaluated once
and shared, tests are reordered for efficiency, and arms that can never be
reached — because an earlier, broader pattern already covers them — are
rejected with compile error CS8510; unlike non-exhaustiveness, a dead
arm doesn’t even compile. The compiler also runs exhaustiveness
analysis on switch expressions over enums and sealed hierarchies,
warning when a value exists that no arm handles.
Gotchas
- An unmatched switch expression throws at runtime. Exhaustiveness
analysis is a warning, not a compile error — ship a build with warnings
ignored and a value that falls through every arm throws
SwitchExpressionException. Always include a_arm unless you have verified exhaustiveness yourself. is T tisfalsefornull. A type pattern never matchesnull, sonot nulland type patterns compose safely —obj is not null and Circle cdoesn’t need a separate null check first.- Property patterns invoke getters. Matching
{ Radius: 0 }calls theRadiusgetter — a side-effecting or expensive getter runs as part of matching, not just when an arm you expect actually executes. - Positional patterns need
Deconstruct. Records synthesize one for free; plain classes and structs don’t unless you write it yourself — attempting a positional pattern on a type withoutDeconstructis a compile error, not a runtime miss. whenguards escape exhaustiveness analysis. The compiler can’t reason about an arbitrary boolean expression, so any arm with awhenclause makes the whole switch look non-exhaustive from its perspective. Prefer relational patterns (> 100,>= 200 and < 300) overwhenwhen the condition can be expressed that way.
Further reading
- Patterns — C# reference
- Pattern matching tutorial
- sharplab.io — see the lowered code yourself