Pattern matching
C# has substantial pattern-matching support, expanded across C# 7, 8, 9, 10, and 11. Patterns may be used with the is operator, in switch statements, and in switch expressions; the pattern grammar admits constants, types, properties, lists (since C# 11), and recursive composition. The full pattern surface is a substantial substitute for the algebraic-data-type pattern matching of functional languages, and modern idiomatic C# uses pattern matching extensively where older code would have used long if/else if chains or visitor-pattern dispatchers.
The mechanism predates C# 7 in the form of type-test casts (if (obj is string)), but the pattern grammar proper began with C# 7’s type patterns and has been extended in every subsequent revision.
The pattern grammar
A pattern matches a value and (optionally) binds parts of it to names. The principal pattern forms:
| Pattern | Example | Matches |
|---|---|---|
| Constant | 42, "x", Color.Red | Equal to the given constant |
| Type | string, Circle c | The value is of the given type (binds c if specified) |
| Property | { Length: > 5 } | The named members satisfy their patterns |
| Var | var x | Binds to x; matches anything |
| Discard | _ | Matches anything; binds nothing |
| Relational | > 0, <= 100 | Compares with <, <=, >, >= |
| Logical | not null, > 0 and < 100 | Combines with and, or, not |
| List | [1, 2, ..], [_, .. var rest] | Matches a list shape (C# 11) |
| Recursive | Circle { Radius: > 0 } | Type pattern plus property pattern |
The grammar is composable: each form may appear inside another, and complex predicates can be written declaratively rather than as a chain of if statements.
Constant patterns
The simplest pattern: equality with a constant:
string Describe(int n) => n switch {
0 => "zero",
1 => "one",
-1 => "minus one",
_ => "other",
};
string ColorName(Color c) => c switch {
Color.Red => "red",
Color.Green => "green",
Color.Blue => "blue",
_ => throw new InvalidOperationException(),
};
Constants admit literals, enum members, named const values, and null. Equality is structural (the == operator) for primitives and reference comparison for reference-typed constants.
Type patterns
A type pattern matches a value of the given type:
if (shape is Circle) { /* shape is a Circle */ }
if (shape is Circle c) {
Console.WriteLine($"radius {c.Radius}");
}
double Area(Shape s) => s switch {
Circle c => Math.PI * c.Radius * c.Radius,
Square sq => sq.Side * sq.Side,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new InvalidOperationException(),
};
The pattern Circle c matches if the value is a Circle (or a subtype) and binds the value to c. The type pattern subsumes both is-test and cast.
Type patterns interact with nullable reference types: matching a non-nullable type pattern (Circle c) excludes null; the bound variable is non-nullable.
Property patterns
A property pattern matches by examining members:
if (person is { Age: > 18 }) {
/* person.Age > 18 */
}
string Describe(Order o) => o switch {
{ Status: OrderStatus.Pending, Total: > 1000 } => "high-value pending",
{ Status: OrderStatus.Shipped } => "shipped",
{ Status: OrderStatus.Cancelled } => "cancelled",
_ => "other",
};
The property pattern { Member: Pattern } matches if the value’s member satisfies the inner pattern. The patterns nest:
if (order is { Customer: { Email: not null } }) {
/* order's customer has a non-null Email */
}
// C# 10 simplifies nested property patterns:
if (order is { Customer.Email: not null }) {
/* same */
}
The C# 10 syntax { Customer.Email: ... } is the conventional contemporary form for nested property access.
Var pattern
The var pattern matches anything and binds:
if (Compute() is var result && result > 0) {
Console.WriteLine($"computed {result}");
}
The construction is occasionally useful for capturing intermediate values within a guard clause. It is rare in routine code; the conventional way to bind a value is an ordinary assignment.
Discard _
The discard pattern matches anything and binds nothing:
string Describe(int n) => n switch {
0 => "zero",
_ => "non-zero",
};
if (TryGet(out var value) is _) { /* ... */ }
The convention is to use _ as the catch-all in switch expressions.
Relational patterns
C# 9 introduced relational patterns:
string Classify(double temp) => temp switch {
< 0 => "freezing",
< 10 => "cold",
< 20 => "cool",
< 30 => "warm",
_ => "hot",
};
if (n is > 0 and < 100) {
/* n in (0, 100) */
}
The patterns admit <, <=, >, >= against constant operands. They compose with logical patterns for ranges.
Logical patterns
C# 9 introduced logical patterns: and, or, not:
if (input is not null) { /* ... */ }
if (n is > 0 and < 100) { /* ... */ }
if (status is StatusCode.Ok or StatusCode.Created) { /* ... */ }
The patterns are first-class in the pattern grammar; they may appear anywhere a pattern is expected and may compose freely with other patterns:
string Describe(object o) => o switch {
null => "null",
string s and { Length: 0 } => "empty string",
string s and { Length: > 100 } => $"long string ({s.Length} chars)",
string => "string",
int n and (> 0 or < 0) => $"non-zero int: {n}",
_ => "other",
};
List patterns
C# 11 introduced list patterns for matching against lists, arrays, and Span<T>:
int[] arr = { 1, 2, 3, 4, 5 };
bool isEmpty = arr is [];
bool isSingleton = arr is [_];
bool startsWithOne = arr is [1, ..];
bool endsWithFive = arr is [.., 5];
bool isOneTwo = arr is [1, 2, ..];
if (arr is [var first, .., var last]) {
Console.WriteLine($"first: {first}, last: {last}");
}
string Describe(int[] xs) => xs switch {
[] => "empty",
[var single] => $"singleton: {single}",
[var first, .., var last] => $"first: {first}, last: {last}",
_ => "other",
};
The list pattern [p1, p2, p3] matches a list of exactly three elements that satisfy the corresponding patterns. The slice pattern .. matches zero or more elements; .. var rest binds the remaining elements to rest. The combination admits compact destructuring of sequences.
Recursive patterns
A pattern may combine type, property, and other patterns:
double Area(Shape s) => s switch {
Circle { Radius: > 0 } c => Math.PI * c.Radius * c.Radius,
Circle { Radius: <= 0 } => 0,
Square { Side: var side } => side * side,
Triangle { Base: var b, Height: var h } => 0.5 * b * h,
null => throw new ArgumentNullException(),
_ => throw new InvalidOperationException(),
};
The pattern Circle { Radius: > 0 } c requires:
- The value is a
Circle. - Its
Radiusis greater than zero. - The matched value is bound to
c.
The mechanism subsumes most of the dispatch patterns of object-oriented and functional languages.
Tuple patterns
A switch may match against a tuple of multiple discriminants:
string CombineStates((Status s1, Status s2) pair) => pair switch {
(Status.Idle, Status.Idle) => "both idle",
(Status.Running, _) => "first running",
(_, Status.Running) => "second running",
_ => "neither running",
};
The form admits compact discrimination over multiple axes — each axis may use any pattern (constants, types, properties, relationals).
Switch expressions
The switch expression combines patterns and arms:
string Classify(object o) => o switch {
null => "null",
int n and > 0 => "positive int",
int n and < 0 => "negative int",
int => "zero int",
string { Length: 0 } => "empty string",
string s => $"string: {s}",
Array a => $"array of {a.Length}",
_ => "other",
};
The arms are evaluated top to bottom; the first matching pattern selects the corresponding expression. Each arm may have a when clause for an arbitrary boolean guard:
double Area(Shape s) => s switch {
Circle c when c.Radius > 0 => Math.PI * c.Radius * c.Radius,
Circle => 0, // radius non-positive
Square sq => sq.Side * sq.Side,
_ => throw new InvalidOperationException(),
};
The compiler tries to determine exhaustiveness; when it cannot, a warning suggests adding a discard arm.
Pattern matching in is
The is operator admits the full pattern grammar:
if (value is int n) { /* n is the value as int */ }
if (value is { Length: > 5 }) { /* value has Length > 5 */ }
if (value is Circle { Radius: > 0 }) { /* ... */ }
if (value is not null) { /* ... */ }
if (value is > 0 and < 100) { /* ... */ }
The is pattern is the conventional contemporary replacement for type-test casts and null-checks. The pattern’s bindings are scoped to the consequent branch:
if (shape is Circle c) {
/* c is in scope */
} else {
/* c is NOT in scope here */
}
The construction generalises to compound conditions:
if (shape is Circle c && c.Radius > 0) {
/* c is in scope; the `&&` extends the binding */
}
Records and pattern matching together
Records (C# 9) integrate cleanly with pattern matching. A positional record pattern matches by deconstruction:
public record Point(double X, double Y);
public record Line(Point Start, Point End);
double LengthSquared(Line line) => line switch {
(Start: (0, 0), End: (var x, var y)) => x * x + y * y,
Line(Start: var s, End: var e) => DistanceSquared(s, e),
};
double DistanceSquared(Point a, Point b) {
var (ax, ay) = a; // record destructuring outside switch
var (bx, by) = b;
var dx = ax - bx;
var dy = ay - by;
return dx * dx + dy * dy;
}
The positional pattern Point(double X, double Y) is the conventional shape for matching records with simple structures; the property pattern { X: ..., Y: ... } is conventional for the more elaborate cases.
Common patterns
Type discrimination
double Process(object value) => value switch {
int n => n * 2.0,
double d => d * 2.0,
string s => s.Length,
null => 0.0,
_ => throw new ArgumentException("unsupported type"),
};
The conventional shape for handling several types in a single function.
Range classification
string Grade(int score) => score switch {
< 0 or > 100 => throw new ArgumentOutOfRangeException(),
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F",
};
The relational pattern is the conventional substitute for nested if/else if cascades.
Null-checking with pattern
if (response is { Body: not null } r) {
/* r is the response, with non-null Body */
UseBody(r.Body);
}
The combination of property pattern and binding admits null-checking and value capture in one expression.
Destructuring records
public record Address(string City, string Country);
if (person.Address is { City: "Berlin" }) {
/* person lives in Berlin */
}
if (line is Line(Point(0, 0), var endPoint)) {
/* line starts at the origin; endPoint is the end */
}
The full pattern grammar admits substantial structural matching — the closest C# comes to algebraic-data-type pattern matching.