Pattern Matching
Patterns test the shape of a value: type, constant, relational, logical, property and list patterns, used by is and by switch.
value is pattern // type, constant, relational, logical, property, listA 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 stringtests the run-time type.value is string textalso hands you astringvariable calledtext. Neither matchesnull. - Constant patterns:
x is 0,s is "exit",x is null. A constant pattern never calls a==operator someone wrote, sois nullalways 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,orandnot, as inn is > 0 and < 10ors 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
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 11When 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.
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: 42Patterns 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.
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: highList 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.
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