Polyglot
Languages C# pattern matching
C# § pattern-matching

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:

PatternExampleMatches
Constant42, "x", Color.RedEqual to the given constant
Typestring, Circle cThe value is of the given type (binds c if specified)
Property{ Length: > 5 }The named members satisfy their patterns
Varvar xBinds to x; matches anything
Discard_Matches anything; binds nothing
Relational> 0, <= 100Compares with <, <=, >, >=
Logicalnot null, > 0 and < 100Combines with and, or, not
List[1, 2, ..], [_, .. var rest]Matches a list shape (C# 11)
RecursiveCircle { 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:

  1. The value is a Circle.
  2. Its Radius is greater than zero.
  3. 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.