Pattern Matching

Patterns test the shape of a value: type, constant, relational, logical, property and list patterns, used by is and by switch.

Syntaxvalue is pattern // type, constant, relational, logical, property, list

A pattern describes a shape a value might have. Rather than writing several separate tests and then converting the value, you write the shape once and let the compiler check it. The is operator takes a value on the left and a pattern on the right, and produces a bool. The same patterns appear in case labels and in switch expression arms, so this is one small language shared by all three.

The patterns

  • Type and declaration patterns: value is string tests the run-time type. value is string text also hands you a string variable called text. Neither matches null.
  • Constant patterns: x is 0, s is "exit", x is null. A constant pattern never calls a == operator someone wrote, so is null always tests the value itself.
  • Relational patterns (C# 9.0): n is > 100. The right-hand side must be a constant.
  • Logical patterns (C# 9.0): and, or and not, as in n is > 0 and < 10 or s is not null.
  • Property patterns (C# 8.0): s is { Length: > 5 } looks inside the value. C# 10.0 allows a path, as in { Start.Y: 0 }.
  • List patterns (C# 11.0): numbers is [1, 2, 3] matches length and position, and .. stands for any number of elements.

What trips people up

A variable introduced by a pattern is in scope for the rest of the block, but the compiler treats it as assigned only where the pattern matched, so reading it in the else branch is an error. And a bare _ is a pattern only in a switch expression arm; in an is expression, write var _.

Example

Example · csharp
object value = "contrails";

// A type pattern checks the run-time type and names the result at once.
if (value is string text)
{
    Console.WriteLine($"a string of {text.Length} characters");
}

// Relational and logical patterns (C# 9.0) read like the test they make.
int temperature = 31;
bool warm = temperature is > 25 and < 40;
Console.WriteLine($"warm: {warm}");

// A property pattern looks inside the value before accepting it.
bool longWord = value is string { Length: > 5 };
Console.WriteLine($"long word: {longWord}");

// is not null is the pattern form of a non-null check.
object? maybe = null;
Console.WriteLine($"present: {maybe is not null}");

// A list pattern (C# 11.0) matches by shape and position.
// .. stands for any number of elements in the middle.
int[] readings = [3, 7, 11];
if (readings is [var first, .., var last])
{
    Console.WriteLine($"first {first}, last {last}");
}

// Output:
// a string of 9 characters
// warm: True
// long word: True
// present: False
// first 3, last 11

When to use it

  • Check that a value really is a string before using its length.
  • Classify a number into a named band in one expression.
  • Accept a command only when it arrived as the right number of words.
  • Test two properties of a value together without reading it twice.

More examples

The type pattern against the longhand it replaces

Older C# code is full of the two-step version, so you need to read it, but write the one-line form. Besides being shorter it cannot drift: there is no way for the test and the conversion to name different types, which is a real bug in the longhand once someone edits one line and not the other.

Example · csharp
object boxed = 42;

// Longhand: test the type, then convert, in two separate steps.
if (boxed is int)
{
    int longhand = (int)boxed;
    Console.WriteLine($"longhand: {longhand}");
}

// Shorthand: one test that also gives you the typed value.
if (boxed is int shorthand)
{
    Console.WriteLine($"shorthand: {shorthand}");
}

// Output:
// longhand: 42
// shorthand: 42

Patterns as the arms of a switch expression

This is where patterns are used most: classifying a value into one of several named cases. Because arms are tried from the top, the ranges do not have to be written to exclude each other, but they do have to be written in a sensible order. Swapping the first two arms changes nothing, since < 0 and 0 do not overlap; what the compiler rejects is an arm that an earlier arm already covers completely, so moving the _ arm to the top would make every arm below it unreachable and the build would fail.

Example · csharp
int[] readings = [-5, 0, 18, 99];

foreach (int reading in readings)
{
    string label = reading switch
    {
        < 0 => "below zero",
        0 => "zero",
        > 0 and < 50 => "normal",
        _ => "high"
    };

    Console.WriteLine($"{reading}: {label}");
}

// Output:
// -5: below zero
// 0: zero
// 18: normal
// 99: high

List patterns over a parsed command

List patterns suit anything that arrives as a short sequence: command-line arguments, a split line of text, the segments of a URL path. They check the length and the contents in one expression, which removes the usual pair of bugs where the length test and the indexing disagree. Remember that a switch expression over list patterns gets no exhaustiveness warning, so include a catch-all arm yourself.

Example · csharp
string[] parts = ["set", "colour", "blue"];

// Match the verb, then capture the two arguments by position.
if (parts is ["set", var key, var newValue])
{
    Console.WriteLine($"set {key} to {newValue}");
}

// _ matches one element; .. var rest captures everything that is left.
if (parts is [_, .. var rest])
{
    Console.WriteLine($"arguments after the verb: {rest.Length}");
}

// A list pattern also fixes the length, so this is a two-item test.
Console.WriteLine(parts is ["set", _]);

// Output:
// set colour to blue
// arguments after the verb: 2
// False

Discussion

  • Be the first to comment on this lesson.