Pattern Matching
Patterns let you test a value's type, shape and contents in a single, readable expression. You've already met a few — is null, is >= 0 and <= 100, switch arms. This lesson brings them all together: type, constant, relational, logical, property, positional and list patterns.
Where patterns are used
Patterns appear in three places: after is, in case labels of a switch statement, and in the arms of a switch expression. Each pattern either matches or doesn't, and may introduce new variables along the way.
Type patterns
Test whether a value has a certain type and, if so, get it as that type in one step:
object[] items = [42, "hello", 3.5, true, null!, new List<int> { 1, 2 }]; foreach (object item in items) { string description = item switch { int n => $"int {n}", string s => $"string of {s.Length} chars", double d => $"double {d}", bool => "a bool", List<int> list => $"list with {list.Count} items", null => "null", _ => "something else", }; Console.WriteLine(description); }
int 42 string of 5 chars double 3.5 a bool null list with 2 items
bool => checks only the type, without introducing a variable.
Constant, relational and logical patterns
Compare against constants with == semantics, or ranges with <, >, <=, >=. Combine patterns with and, or and not, using parentheses where needed:
foreach (char c in "aZ7 ?é") { string kind = c switch { >= 'a' and <= 'z' => "lower-case letter", >= 'A' and <= 'Z' => "upper-case letter", >= '0' and <= '9' => "digit", ' ' or '\t' => "whitespace", not (>= ' ' and <= '~') => "not plain ASCII", _ => "punctuation", }; Console.WriteLine($"'{c}': {kind}"); }
'a': lower-case letter 'Z': upper-case letter '7': digit ' ': whitespace '?': punctuation 'é': not plain ASCII
The same patterns work after is, which reads almost like English:
int age = 15; string? name = "Tia"; DayOfWeek day = DayOfWeek.Sunday; Console.WriteLine(age is >= 13 and <= 19); Console.WriteLine(name is not null and not ""); Console.WriteLine(day is DayOfWeek.Saturday or DayOfWeek.Sunday);
True True True
Property patterns
A property pattern looks inside an object: { Property: pattern, … }. Nested properties can be written with dots:
Order[] orders =
[
new(120m, "UK", new Customer("Ana", IsVip: true)),
new(45m, "UK", new Customer("Ben", IsVip: false)),
new(300m, "US", new Customer("Cy", IsVip: false)),
new(20m, "FR", new Customer("Di", IsVip: true)),
];
foreach (Order o in orders)
{
decimal shipping = o switch
{
{ Customer.IsVip: true } => 0m,
{ Country: "UK", Total: >= 100 } => 0m,
{ Country: "UK" } => 4.99m,
{ Total: > 250 } => 9.99m,
_ => 19.99m,
};
Console.WriteLine($"{o.Customer.Name}: shipping {shipping}");
}
record Customer(string Name, bool IsVip);
record Order(decimal Total, string Country, Customer Customer);
Ana: shipping 0 Ben: shipping 4.99 Cy: shipping 9.99 Di: shipping 0
Arms are tried in order, so put the most specific rules first. Business rules like this are much easier to read as patterns than as nested if statements.
An empty property pattern { } matches any non-null value, which makes if (x is { } value) a compact "not null, and give me a variable" check.
Positional patterns
Types that can be deconstructed — records, tuples — can be matched by position:
Console.WriteLine(Quadrant(new Point(3, 4))); Console.WriteLine(Quadrant(new Point(-1, 5))); Console.WriteLine(Quadrant(new Point(0, 0))); Console.WriteLine(Quadrant(new Point(2, 0))); static string Quadrant(Point p) => p switch { (0, 0) => "origin", (_, 0) or (0, _) => "on an axis", ( > 0, > 0) => "first quadrant", ( < 0, > 0) => "second quadrant", ( < 0, < 0) => "third quadrant", _ => "fourth quadrant", }; record Point(int X, int Y);
first quadrant second quadrant origin on an axis
Tuples make it easy to match several values together — a neat way to write state machines:
var state = DoorState.Closed; foreach (string action in new[] { "open", "lock", "close", "lock", "open", "unlock" }) { state = (state, action) switch { (DoorState.Closed, "open") => DoorState.Open, (DoorState.Open, "close") => DoorState.Closed, (DoorState.Closed, "lock") => DoorState.Locked, (DoorState.Locked, "unlock") => DoorState.Closed, _ => state, // ignore invalid actions }; Console.WriteLine($"{action,-7} -> {state}"); } enum DoorState { Open, Closed, Locked }
open -> Open lock -> Open close -> Closed lock -> Locked open -> Locked unlock -> Closed
List patterns
List patterns match arrays and lists by their elements. .. matches any number of elements (and can capture them), and _ matches exactly one:
int[][] inputs = [[], [7], [1, 2], [1, 2, 3, 4], [5, 0, 0, 5]]; foreach (int[] a in inputs) { string shape = a switch { [] => "empty", [var only] => $"one element: {only}", [var first, .., var last] when first == last => $"starts and ends with {first}", [1, 2, .. var rest] => $"starts 1, 2 then {rest.Length} more", [_, _] => "two elements", _ => "something else", }; Console.WriteLine($"[{string.Join(",", a)}] {shape}"); }
[] empty [7] one element: 7 [1,2] starts 1, 2 then 0 more [1,2,3,4] starts 1, 2 then 2 more [5,0,0,5] starts and ends with 5
List patterns are excellent for simple command parsing:
foreach (string line in new[] { "add milk 2", "remove eggs", "list", "help me" }) { string[] words = line.Split(' '); string reply = words switch { ["add", var item, var qty] when int.TryParse(qty, out int n) => $"Adding {n} x {item}", ["add", var item] => $"Adding 1 x {item}", ["remove", var item] => $"Removing {item}", ["list"] => "Listing items", _ => $"Unknown command: {line}", }; Console.WriteLine(reply); }
Adding 2 x milk Removing eggs Listing items Unknown command: help me
when clauses
When a condition can't be expressed as a pattern — comparing two captured values, calling a method — add when, as in the examples above. Patterns themselves must use constants; when can use any expression.
Patterns cheat sheet
| Pattern | Example | Matches |
|---|---|---|
| Type | string s | Value of that type; binds s |
| Constant | 0, "yes", null | Equal to the constant |
| Relational | >= 18 | Comparison is true |
| Logical | not null, 1 or 2, > 0 and < 10 | Combination of patterns |
| Property | { Length: > 3 } | Properties match their patterns |
| Positional | (0, _) | Deconstructed values match |
| List | [1, .., var last] | Elements match in order |
| var | var x | Anything, including null; binds it |
| Discard | _ | Anything |
Try it yourself
Classify triangles from their three side lengths using a tuple and patterns.
(3, 3, 3): equilateral (3, 3, 5): isosceles (3, 4, 5): scalene (1, 2, 8): impossible (0, 2, 2): invalid: non-positive side