C# & .NET (C# 14, .NET 10 LTS) Module 10 – Pattern matching
Relational, logical and list patterns
Compare values with C# relational patterns, combine tests with and, or and not, and match arrays, lists and spans by their shape with list and slice patterns.
What you will learn
- Match ranges of values with relational patterns and combine them with and, or and not
- Predict how not, and and or group without brackets, and fix a test that groups wrongly
- Match arrays, lists and strings by shape with list and slice patterns
- Find the values a set of number ranges leaves uncovered, using the compiler's warnings
- Parse text with list patterns on spans, and measure what a captured slice allocates
Before you start
What you’ll learn · Before you start
What you will learn
- Match ranges of values with relational patterns and combine them with and, or and not
- Predict how not, and and or group without brackets, and fix a test that groups wrongly
- Match arrays, lists and strings by shape with list and slice patterns
- Find the values a set of number ranges leaves uncovered, using the compiler's warnings
- Parse text with list patterns on spans, and measure what a captured slice allocates
Before you start
On this page
A cold-room sensor reports 75.0 °C. The room should stay from 2 to 8 degrees and the sensor cannot measure above 60, so the reading is a fault, not a temperature. The test that flags it is one line of relational and logical patterns, and one pair of brackets decides whether it works.
Patterns can also test the shape of a sequence: how many arguments a command has, and which word comes first.
Relational and logical patterns compare with constants and combine tests
< 2 and <= 8 are relational patterns, met in the lesson on switch: they compare the value with a constant of a
numeric, char or enum type. logical pattern: A pattern built from other patterns with and, or and not, such as >= 2 and <= 8 or not null, added in C# 9. not binds first, then and, then or; brackets change the grouping., added in C# 9 with relational ones,
combine other patterns: and needs both to match, or either, and not turns a match into a miss. coldroom.cs
sorts eight readings into bands, then tests each one for a fault twice, with brackets and without:
// Readings in degrees Celsius from a cold-room sensor. Relational patterns sort them into bands; logical patterns
// flag readings the sensor cannot really produce. The room should stay from 2 to 8; the sensor measures -30 to 60.
double[] readings = [4.2, 2.0, 1.5, 9.1, 12.0, -0.5, 75.0, double.NaN];
foreach (double celsius in readings)
{
Console.WriteLine($"{celsius,5} {Band(celsius),-10} fault: {Fault(celsius),-5} fault, written without brackets: {FaultSlip(celsius)}");
}
static string Band(double celsius) => celsius switch
{
double.NaN => "no reading",
< 0 => "freezing",
< 2 => "too cold",
<= 8 => "in range",
<= 10 => "warm",
_ => "too warm",
};
// A reading outside the sensor's range is a fault, not a temperature.
static bool Fault(double celsius) => celsius is not (>= -30.0 and <= 60.0);
// The same test without brackets: not binds to >= -30.0 alone, so this means (< -30.0 or NaN) and <= 60.0.
static bool FaultSlip(double celsius) => celsius is not >= -30.0 and <= 60.0; Output
4.2 in range fault: False fault, written without brackets: False
2 in range fault: False fault, written without brackets: False
1.5 too cold fault: False fault, written without brackets: False
9.1 warm fault: False fault, written without brackets: False
12 too warm fault: False fault, written without brackets: False
-0.5 freezing fault: False fault, written without brackets: False
75 too warm fault: True fault, written without brackets: False
NaN no reading fault: True fault, written without brackets: False
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run coldroom.cs
Read the bands from the top. Every arm names only where its band ends, since whatever lies below that end was
already taken by an arm above, so 2.0 fell into <= 8 and printed in range. NaN needed its own arm, double.NaN, a
constant pattern, because a relational pattern never matches NaN. The two fault columns disagree on two readings,
75.0 and NaN.
The cause is how the combinators group. The C# reference gives the order: not binds first, then and, then or.
Without brackets, not >= -30.0 and <= 60.0 means (not >= -30.0) and <= 60.0, which is not the range test it looks
like. Step through both groupings for the two readings they disagree on:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- not binds first. Pattern combinators bind in the order not, then and, then or. Without brackets, not takes only the pattern right after it, >= -30.0, and the and joins that result to <= 60.0.
- Brackets group the range. With brackets the range >= -30.0 and <= 60.0 is one pattern, and not flips its result: true for every reading outside the range the sensor can measure.
- 75.0 without brackets. 75.0 is at least -30.0, so not >= -30.0 is false, and false and anything is false. FaultSlip reports 75.0 as no fault.
- 75.0 with brackets. 75.0 is above 60.0, so the range is false, and not makes it true. Fault reports 75.0 as a fault, as it should.
- NaN without brackets. NaN fails every relational pattern. not >= -30.0 is therefore true, but <= 60.0 is false, so FaultSlip misses the missing reading as well.
- NaN with brackets. For NaN the range is false, because both its comparisons fail, and not makes it true: Fault flags the missing reading too.
Writing not before a range without brackets
celsius is not >= -30.0 and <= 60.0 negates only the first comparison, so 75.0 and NaN pass as no fault. Write
celsius is not (>= -30.0 and <= 60.0): the brackets make the range one pattern, and not then covers all of it.
One more rule follows from or and not: a pattern under them cannot declare a variable, error CS8780, because the
compiler could not tell whether it was assigned. The single exception is the one met in the first lesson,
is not string text as the whole pattern of an is.
Checkpoint
Check: Relational and logical patterns, and how they group
A list pattern matches a sequence by its length and its elements
A list pattern: A pattern in square brackets, such as ["add", var item], that matches a sequence of that length element by element (C# 11). The compile-time type needs a Length or Count and an indexer., added in C# 11, is a list of patterns in square brackets.
["add", var item] matches a sequence of exactly two elements whose first is "add", and names the second.
[] matches an empty sequence, and a slice pattern: The pattern .. inside a list pattern, at most once, standing for any number of elements, as in [first, .., last]. Followed by var or another pattern, it captures those elements as a slice., .., stands for any number of
elements, at most once per list pattern. commands.cs reads the arguments of a shopping-list tool:
// The arguments of a shopping-list tool, matched by their shape with list patterns: how many there are, and what
// stands at each position.
string[][] calls =
[
["add", "mango"],
["add", "rice", "5", "kg"],
["remove", "mango"],
["list"],
[],
["remove"],
["clear", "--yes"],
];
foreach (string[] call in calls)
{
Console.WriteLine($"{Show(call),-27} {Describe(call)}");
}
static string Describe(string[] args) => args switch
{
[] => "no command: print the help",
["add", var item] => $"add {item}",
["add", var item, .. var details] => $"add {item}, with {details.Length} more words: {string.Join(' ', details)}",
["remove", var item] => $"remove {item}",
["list"] => "list everything",
[var command, ..] => $"unknown or incomplete: {command}",
};
static string Show(string[] args) => "[" + string.Join(", ", args.Select(a => $"\"{a}\"")) + "]"; Output
["add", "mango"] add mango ["add", "rice", "5", "kg"] add rice, with 2 more words: 5 kg ["remove", "mango"] remove mango ["list"] list everything [] no command: print the help ["remove"] unknown or incomplete: remove ["clear", "--yes"] unknown or incomplete: clear
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run commands.cs
Each call took the first arm whose shape fitted. ["add", "rice", "5", "kg"] was too long for ["add", var item],
so it reached the arm with a slice, where .. var details captured the last two words. ["remove"] was too short for
["remove", var item] and fell to [var command, ..], which accepts any call with at least one word. Step through
the long add call:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The arm above fails. The arm ["add", var item] has no slice, so it needs exactly two tokens. The call has four, so that arm fails on its length before any token is compared.
- Length first. The next arm has two element patterns and a slice, so it needs at least two tokens. Four is enough.
- The command word. The constant pattern "add" is matched against the token at index 0. It is the same string, so this part matches.
- The item. var item matches any token and names the one at index 1: item is "rice".
- The slice. The slice takes every token between the patterns before and after it, here the last two. Captured with var, they are named details: the 2 tokens "5" and "kg".
List patterns work on arrays, List<T>, strings and spans: any type whose compile-time type has a Length or Count
and an indexer. An IEnumerable<T> alone does not qualify, because the pattern reads positions instead of
enumerating, and capturing a slice with var also needs a range indexer or a Slice method.
| Pattern | Tests | Example | Added in |
|---|---|---|---|
| relational | a comparison with a constant | <= 8 |
C# 9 |
| logical | a combination of patterns | not (>= -30.0 and <= 60.0) |
C# 9 |
| list | a sequence’s length and elements | ["add", var item, ..] |
C# 11 |
Two questions build on this: which values a set of number ranges leaves uncovered, which the compiler can name for you, and what a captured slice costs on strings, arrays and spans.
Checkpoint
Check: List and slice patterns
Gaps in number ranges: what the compiler names, and the slip it cannot see Pro: opens with a Pro, Premium or Ultimate pass
A switch over numbers can leave values uncovered between its arms, and the compiler searches for them. This part builds four tests that draw its warnings and one that draws none, then gives the rules that close every gap.
In this part:
- Four warnings over numbers, and one slip with none, built and run
- Where each boundary falls, walked a tenth of a degree at a time
- Writing bands that leave no gap
3 runnable examples (C#, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Parsing with list patterns on strings and spans, with allocations measured Pro: opens with a Pro, Premium or Ultimate pass
A list pattern reads elements by position, so matching a sequence costs nothing extra, but capturing a slice can. This part counts the bytes three versions of one test allocate, then gives you a chat command parser to write.
In this part:
- What a captured slice allocates, counted over 1,000 lines
- A clock time checked character by character, without allocating
- Exercise: chat commands into typed records
3 runnable examples (C#) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: relational, logical and list patterns
Practise the three kinds of pattern below. Every choice says why it is right or wrong, and the kind you are least sure of is asked about first:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What do the and, or and not patterns do, and in what order do they apply?
They combine patterns, added in C# 9 with relational patterns: and matches when both patterns match, or when
either does, and not when its pattern does not. They bind in that order of strength, not first, then and, then
or, so x is not >= 10 and <= 20 means (not >= 10) and <= 20, which is not a range test. Use brackets to group,
as in x is not (>= 10 and <= 20). Unlike && and ||, they combine patterns, not Boolean expressions, and a
variable cannot be declared under or.
-
Mid to senior
A cold-room alert fires when reading is not >= -30.0 and <= 60.0. It has never fired for an impossible reading, although the logs hold values of 75 and NaN, and its unit tests at -40 and 20 pass. Explain the bug, why the build gave no warning, and how you would test rules like this.
-
Senior to staff
A service parses five million log lines a day with string.Split and Substring, and a colleague proposes rewriting the parser with list patterns. Would you? Explain what list patterns make simpler, where they do not fit, what they cost on strings and spans, and how you would measure the change.
-
Fresher to mid
A delivery fee is a switch over whole kilometres with the arms < 5, >= 5 and < 10, and > 10. One order made it throw SwitchExpressionException in production. Which distance was it, why did the build not stop, and what would you change in the code and in the build?
Key takeaways
- Relational patterns compare with a constant, and each arm of a band switch states only its top; NaN needs its own
double.NaNarm. notbinds beforeand, which binds beforeor, so brackets decide what anotcovers.- A list pattern tests a sequence’s length and its elements by position, with one optional slice,
.., for the elements in between. - List patterns need a
LengthorCountand an indexer on the compile-time type, so arrays, lists, strings and spans qualify and a plainIEnumerable<T>does not.
Terms in this lesson
- list pattern
- A pattern in square brackets, such as ["add", var item], that matches a sequence of that length element by element (C# 11). The compile-time type needs a Length or Count and an indexer.
- logical pattern
- A pattern built from other patterns with and, or and not, such as >= 2 and <= 8 or not null, added in C# 9. not binds first, then and, then or; brackets change the grouping.
- slice pattern
- The pattern .. inside a list pattern, at most once, standing for any number of elements, as in [first, .., last]. Followed by var or another pattern, it captures those elements as a slice.
References
- Relational, logical, and parenthesized patterns (Microsoft Learn)
- List and slice patterns (Microsoft Learn)
- Patterns: relational, logical and list patterns (Microsoft Learn)
- C# language specification: patterns and pattern matching (Microsoft Learn (ECMA-334 draft))
- Resolve pattern matching errors and warnings (Microsoft Learn)
- GC.GetAllocatedBytesForCurrentThread method (Microsoft Learn)
- The history of C# (Microsoft Learn)
Report a problem with this lesson
Kept only in this browser. Your Learn progress