diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index 70deeda36e76d..9b20b385deefa 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -69,6 +69,8 @@ Relational operators compare two values and return a `bool`. Relational operators work on all numeric types and `char`. For `char`, comparison uses the character's numeric Unicode code point value, not any alphabetical or domain-specific ordering. In the grade example above, `'B'` is greater than or equal to `'A'` because `'B'` has Unicode value 66 and `'A'` has Unicode value 65 — the *numbers* determine the comparison, not the meaning of the letter grades. +The same symbols can form [relational patterns](../patterns/relational-logical-patterns.md) in an `is` expression or `switch`. For example, `temperature < 0` is a relational expression that returns a `bool`, while `temperature is < 0` applies the relational pattern `< 0` to the value of `temperature`. + ## Equality operators `==` and `!=` check whether two values are equal or not. `!=` is `true` when the operands are **not** equal, and `false` when they are. diff --git a/docs/csharp/fundamentals/patterns/list-patterns.md b/docs/csharp/fundamentals/patterns/list-patterns.md new file mode 100644 index 0000000000000..5057378b58454 --- /dev/null +++ b/docs/csharp/fundamentals/patterns/list-patterns.md @@ -0,0 +1,64 @@ +--- +title: "List and slice patterns" +description: Learn when to use C# list patterns to test a sequence's shape and selected elements, and slice patterns to allow unmatched elements. +ms.date: 09/17/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# List and slice patterns + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. Start with the [pattern matching overview](pattern-matching.md) if patterns are new to you. For complete supported-type and language rules, see [list patterns](../../language-reference/operators/patterns.md#list-patterns) in the language reference. + +A *list pattern* tests the shape of an array, list, or another supported sequence and applies nested patterns to selected elements. Shape includes the number and positions of elements. A *slice pattern*, written `..`, allows a list pattern to contain zero or more elements that aren't tested individually. + +List patterns don't make every input matchable. The input's compile-time type must support the length or count and element access required by list-pattern rules. Arrays, `List`, strings, and spans are common examples. + +## Match an exact shape + +The following method recognizes a two-column header: + +:::code language="csharp" source="snippets/patterns/ListPatterns.cs" ID="ExactListPattern"::: + +The `columns` expression is the pattern input. `["Name", "Score"]` contains two constant patterns. Without a slice pattern, the length must be exactly two, and each nested pattern must match the element in the same position. A longer array doesn't match even if its first two elements are the same. + +Choose a list pattern when both the sequence shape and selected element values express the decision. If only the number of elements matters, a `Length` or `Count` property pattern, such as `items is { Count: 0 }`, states that intent more directly. + +## Match selected elements with discards + +The following method reads the winner and third-place finisher from a three-name finishing order: + +:::code language="csharp" source="snippets/patterns/ListPatterns.cs" ID="CaptureElements"::: + +`var winner` and `var thirdPlace` capture elements that the result uses. The discard pattern `_` accepts the second element without retaining it. Because there's no `..`, the list must contain exactly three elements. + +Choose this form when fixed positions have stable meaning. Use a loop or LINQ when you need to inspect an arbitrary number of elements, transform a sequence, search throughout it, or perform aggregation. + +## Allow remaining elements with a slice pattern + +A command line can start with `--verbose`, contain other arguments, and end with the input file name. The following method recognizes that shape and captures the file name: + +:::code language="csharp" source="snippets/patterns/ListPatterns.cs" ID="SlicePattern"::: + +The slice pattern `..` matches zero or more elements between the first and last elements. A list pattern can contain at most one slice pattern. In this example, the program doesn't need the middle arguments, so the slice has no nested pattern or variable. + +A slice can appear at the beginning, middle, or end of a list pattern. Use it when the elements around the slice are the meaningful part of the shape. Don't use a list pattern to replace ordinary iteration when every element needs processing. + +## Apply a pattern to a slice + +You can apply another pattern to the part matched by `..`. The following method tests whether an array starts with `"BEGIN"`, ends with `"END"`, and has at least one element between them: + +:::code language="csharp" source="snippets/patterns/ListPatterns.cs" ID="SliceSubpattern"::: + +The outer pattern first requires `"BEGIN"` and `"END"` at the boundaries. The property pattern `{ Length: > 0 }` then tests the slice between them. + +Use a slice subpattern only when the middle portion itself needs a test or capture. If only boundary elements matter, plain `..` is simpler. + +## See also + +- [Pattern matching overview](pattern-matching.md) +- [Property and positional patterns](property-positional-patterns.md) +- [List pattern reference](../../language-reference/operators/patterns.md#list-patterns) +- [Arrays](../../language-reference/builtin-types/arrays.md) +- [Use a `foreach` statement to iterate through a collection](../statements/collections.md) diff --git a/docs/csharp/fundamentals/patterns/pattern-matching.md b/docs/csharp/fundamentals/patterns/pattern-matching.md index e7d4d8e09ffd6..3f5ae6d6edeaf 100644 --- a/docs/csharp/fundamentals/patterns/pattern-matching.md +++ b/docs/csharp/fundamentals/patterns/pattern-matching.md @@ -79,17 +79,20 @@ C# includes patterns for common kinds of data tests: | --- | --- | | [Declaration, constant, and `var` patterns](declaration-constant-var-patterns.md) | A run-time type, a specific constant value, or any value that you want to capture | | [Type patterns](type-patterns.md) | A run-time type without declaring a variable | -| Property and positional patterns | Properties, fields, or deconstructed values | -| Relational and logical patterns | Comparisons and combinations such as `and`, `or`, and `not` | -| List patterns | The values and shape of a list or array | +| [Property and positional patterns](property-positional-patterns.md) | Properties, fields, or deconstructed values | +| [Relational, logical, and parenthesized patterns](relational-logical-patterns.md) | Comparisons and combinations such as `and`, `or`, and `not` | +| [List and slice patterns](list-patterns.md) | The values and shape of a supported sequence | | [Discard patterns and discards](discards.md) | Any remaining value, or a value your code intentionally ignores | -The Fundamentals articles linked in the table provide focused coverage of the categories currently documented in this section. For complete syntax and examples for all pattern categories, see the [patterns reference](../../language-reference/operators/patterns.md). +The linked Fundamentals articles explain when to choose each category. For complete syntax and examples, see the [patterns reference](../../language-reference/operators/patterns.md). ## See also - [Declaration, constant, and `var` patterns](declaration-constant-var-patterns.md) - [Type patterns](type-patterns.md) +- [Property and positional patterns](property-positional-patterns.md) +- [Relational, logical, and parenthesized patterns](relational-logical-patterns.md) +- [List and slice patterns](list-patterns.md) - [Discards](discards.md) - [Patterns reference](../../language-reference/operators/patterns.md) - [`switch` expression reference](../../language-reference/operators/switch-expression.md) diff --git a/docs/csharp/fundamentals/patterns/property-positional-patterns.md b/docs/csharp/fundamentals/patterns/property-positional-patterns.md new file mode 100644 index 0000000000000..4762351f79ab1 --- /dev/null +++ b/docs/csharp/fundamentals/patterns/property-positional-patterns.md @@ -0,0 +1,67 @@ +--- +title: "Property and positional patterns" +description: Learn when to use C# property patterns to test named members and positional patterns to test deconstructed or tuple values. +ms.date: 09/17/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# Property and positional patterns + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. Start with the [pattern matching overview](pattern-matching.md) if patterns are new to you. For complete language rules, see [property patterns](../../language-reference/operators/patterns.md#property-pattern) and [positional patterns](../../language-reference/operators/patterns.md#positional-pattern) in the language reference. + +Property and positional patterns test parts of a value. A *property pattern* names the properties or fields to test. A *positional pattern* tests values produced by deconstructing an object or tuple. + +Both are *recursive patterns*: Each member or position has its own nested pattern. The input to the outer pattern is an expression. C# evaluates that expression, then applies each nested pattern to the corresponding part of the evaluated value. + +## Test named members with a property pattern + +The following method tests two named properties of a weather reading, with temperature values in degrees Celsius: + +:::code language="csharp" source="snippets/patterns/PropertyPositionalPatterns.cs" ID="PropertyPattern"::: + +The `reading` expression is the input to the outer pattern. The pattern matches only when its evaluated value is non-null and both nested patterns match: + +- The relational pattern `> 30` tests the value of `TemperatureC`. +- The relational pattern `> 70` tests the value of `HumidityPercent`. + +Choose a property pattern when member names help explain the test. Unlike a series of Boolean expressions, the pattern groups the relevant shape and values in one description. For one simple comparison, such as `reading.TemperatureC > 30`, an ordinary relational expression is often clearer. + +You can add a type test before the braces when the input expression can produce different types. You can also use a member path to test a nested property: + +:::code language="csharp" source="snippets/patterns/PropertyPositionalPatterns.cs" ID="NestedPropertyPattern"::: + +`DateTime { Date.DayOfWeek: DayOfWeek.Saturday or DayOfWeek.Sunday }` first tests that the evaluated value is a . It then follows the `Date.DayOfWeek` member path and tests the day against two constant patterns. The pattern doesn't match if the outer value is `null` or the type test fails. In general, a property pattern also doesn't match if an object needed along a member path is `null`. + +Choose named properties over positions when readers would need to memorize what each position means. + +## Test a stable shape with a positional pattern + +A *positional pattern* deconstructs a value and applies nested patterns in order. A type can define that order with a `Deconstruct` method. Positional records provide deconstruction automatically. + +The following `GridPoint` record has an `X` coordinate followed by a `Y` coordinate. The method classifies a point by its position relative to the axes: + +:::code language="csharp" source="snippets/patterns/PropertyPositionalPatterns.cs" ID="PositionalPattern"::: + +The `point` expression is the pattern input. For `(0, 0)`, C# evaluates `point`, deconstructs the non-null value into its `X` and `Y` components, and applies a constant pattern to each component. The discard pattern `_` accepts a component that doesn't matter to that arm. + +The positions must follow the type's deconstruction order. Choose a positional pattern when that order is a deliberate, stable part of the type's design, such as `(X, Y)`. Use a property pattern when names communicate the test better or when the deconstruction order is difficult to remember. + +## Match a tuple of related inputs + +A tuple combines multiple values into one value with a fixed positional shape. The following method uses a signal value and a Boolean value to choose one result: + +:::code language="csharp" source="snippets/patterns/PropertyPositionalPatterns.cs" ID="TuplePattern"::: + +The tuple expression `(signal, crossingIsClear)` is the input. Each switch arm applies a positional pattern to both tuple elements. This form keeps each combination next to its result. + +Choose a tuple pattern when several small, related inputs jointly determine one result. If the positions need extensive explanation or the data belongs together throughout the program, define a type with named properties instead. + +## See also + +- [Pattern matching overview](pattern-matching.md) +- [Relational, logical, and parenthesized patterns](relational-logical-patterns.md) +- [Deconstructing tuples and other types](../functional/deconstruct.md) +- [Property pattern reference](../../language-reference/operators/patterns.md#property-pattern) +- [Positional pattern reference](../../language-reference/operators/patterns.md#positional-pattern) diff --git a/docs/csharp/fundamentals/patterns/relational-logical-patterns.md b/docs/csharp/fundamentals/patterns/relational-logical-patterns.md new file mode 100644 index 0000000000000..5112e831af468 --- /dev/null +++ b/docs/csharp/fundamentals/patterns/relational-logical-patterns.md @@ -0,0 +1,80 @@ +--- +title: "Relational, logical, and parenthesized patterns" +description: Learn how C# relational patterns compare values and how logical and parenthesized patterns combine pattern tests. +ms.date: 09/17/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# Relational, logical, and parenthesized patterns + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. Start with the [pattern matching overview](pattern-matching.md) if patterns are new to you. For complete language rules, see [relational patterns](../../language-reference/operators/patterns.md#relational-patterns) and [logical patterns](../../language-reference/operators/patterns.md#logical-patterns) in the language reference. + +A *relational pattern* compares an evaluated value with a constant by using `<`, `>`, `<=`, or `>=`. *Logical patterns* combine or negate patterns with the pattern operators `and`, `or`, and `not`. A *parenthesized pattern* uses parentheses to make the intended grouping explicit or to change the default grouping. + +## Distinguish expressions from patterns + +The same relational symbol can appear in an ordinary expression or in a pattern. The following example uses both forms with a temperature: + +:::code language="csharp" source="snippets/patterns/RelationalLogicalPatterns.cs" ID="ExpressionAndPattern"::: + +`temperature < 0` is a *relational expression*. It has a left operand and a right operand, and produces a `bool`. + +In `temperature is < 0`, `temperature` is the pattern input expression. C# evaluates it, and the relational pattern `< 0` tests the resulting value. In the switch arm `< 0 => "Freezing"`, the expression before `switch` supplies the input, so the pattern contains only `< 0`. + +The example displays both Boolean results to show that the two tests classify the same temperature. The switch expression maps the value to a description. + +Choose a relational expression for one direct comparison. Choose relational patterns when the comparison is part of a larger pattern or when several ranges map cleanly to switch results. + +## Describe ranges with `and` + +The following pattern tests whether a temperature is in the inclusive range from 18 through 24: + +:::code language="csharp" source="snippets/patterns/RelationalLogicalPatterns.cs" ID="AndPattern"::: + +The input expression is `temperature`. The logical pattern `>= 18 and <= 24` combines two relational patterns that both test the same evaluated value. The `and` pattern matches only when both nested patterns match. + +`and` is a pattern operator here, not the conditional-AND Boolean operator `&&`. Pattern matching describes what must match. Don't rely on nested patterns being tested left to right or short-circuiting like Boolean operands. + +## Describe alternatives with `or` and exclusions with `not` + +The following methods test a day of the week and a simple status value: + +:::code language="csharp" source="snippets/patterns/RelationalLogicalPatterns.cs" ID="OrNotPatterns"::: + +`DayOfWeek.Saturday or DayOfWeek.Sunday` is one logical pattern composed of two constant patterns. It matches when either nested pattern matches. `IsActive` uses the `not` pattern to exclude `Status.Complete`. + +`not` is a pattern operator, not the Boolean negation operator `!`. Choose `or` when several pattern alternatives have the same result. Choose `not` when expressing the excluded pattern is clearer than listing every accepted value. + +## Group patterns with parentheses + +Pattern operators bind in this order: + +1. `not` +1. `and` +1. `or` + +The following test accepts priorities 1 through 3 or the special priority 9: + +:::code language="csharp" source="snippets/patterns/RelationalLogicalPatterns.cs" ID="ParenthesizedPattern"::: + +The parentheses aren't required for the compiler because `and` binds before `or`, but they make the two alternatives visible: the range from 1 through 3, or 9. Use parentheses whenever a reader might hesitate over the grouping. Parentheses can also change the default grouping, such as `not (>= 1 and <= 3)`. + +## Use a `when` guard for a separate condition + +Logical patterns work best when nested patterns describe the input value itself. A `when` guard is an additional Boolean condition on a `case` label or switch arm. Use a guard when the decision also depends on information that isn't naturally part of the pattern. + +The following warning depends on the temperature and a separate `isOutdoors` value: + +:::code language="csharp" source="snippets/patterns/RelationalLogicalPatterns.cs" ID="WhenGuard"::: + +The relational pattern `> 35` describes the `temperature` input. The guard `when isOutdoors` checks a separate value. A guard is also preferable when the condition needs a method call or a Boolean expression that pattern syntax doesn't express clearly. + +## See also + +- [Pattern matching overview](pattern-matching.md) +- [Property and positional patterns](property-positional-patterns.md) +- [C# operators](../expressions/operators.md) +- [Relational pattern reference](../../language-reference/operators/patterns.md#relational-patterns) +- [Logical and parenthesized pattern reference](../../language-reference/operators/patterns.md#logical-patterns) diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/ListPatterns.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/ListPatterns.cs new file mode 100644 index 0000000000000..9c3279df8e872 --- /dev/null +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/ListPatterns.cs @@ -0,0 +1,42 @@ +static class ListPatterns +{ + public static void Run() + { + Console.WriteLine($"Header: {IsHeader(["Name", "Score"])}"); + Console.WriteLine(GetAnnouncements(["Mina", "Luis", "Ada"])); + Console.WriteLine( + GetInputFile(["--verbose", "--safe", "report.csv"])); + Console.WriteLine( + $"Has content: {HasContent(["BEGIN", "value", "END"])}"); + } + + // + static bool IsHeader(string[] columns) => + columns is ["Name", "Score"]; + // + + // + static string GetAnnouncements(List finishingOrder) => + finishingOrder switch + { + [var winner, _, var thirdPlace] => + $"Winner: {winner}; third place: {thirdPlace}", + _ => "A complete three-runner result isn't available" + }; + // + + // + static string GetInputFile(string[] arguments) => + arguments switch + { + ["--verbose", .., var fileName] => $"Verbose processing: {fileName}", + [.., var fileName] => $"Processing: {fileName}", + [] => "No input file was provided" + }; + // + + // + static bool HasContent(string[] entries) => + entries is ["BEGIN", .. { Length: > 0 }, "END"]; + // +} diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs index c55bbc0a6b086..8aea8ed7ab1e2 100644 --- a/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs @@ -1,3 +1,6 @@ Overview.Run(); BasicPatterns.Run(); TypePatterns.Run(); +PropertyPositionalPatterns.Run(); +RelationalLogicalPatterns.Run(); +ListPatterns.Run(); diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/PropertyPositionalPatterns.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/PropertyPositionalPatterns.cs new file mode 100644 index 0000000000000..14016023b5d07 --- /dev/null +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/PropertyPositionalPatterns.cs @@ -0,0 +1,62 @@ +static class PropertyPositionalPatterns +{ + public static void Run() + { + Console.WriteLine($"Hot and humid: {IsHotAndHumid( + new WeatherReading(32, 75))}"); + Console.WriteLine($"Date: {DescribeDate( + new DateTime(2026, 9, 19))}"); + Console.WriteLine($"Point: {ClassifyPoint(new GridPoint(0, 5))}"); + Console.WriteLine($"Crossing: {GetCrossingInstruction( + PedestrianSignal.Walk, crossingIsClear: true)}"); + } + + // + static bool IsHotAndHumid(WeatherReading reading) => + reading is { TemperatureC: > 30, HumidityPercent: > 70 }; + + sealed record WeatherReading(int TemperatureC, int HumidityPercent); + // + + // + static string DescribeDate(object? value) => + value switch + { + DateTime { Date.DayOfWeek: + DayOfWeek.Saturday or DayOfWeek.Sunday } => "Weekend date", + DateTime => "Weekday date", + null => "No date", + _ => "Not a date" + }; + // + + // + static string ClassifyPoint(GridPoint point) => + point switch + { + (0, 0) => "Origin", + (0, _) => "On the vertical axis", + (_, 0) => "On the horizontal axis", + _ => "Away from both axes" + }; + + readonly record struct GridPoint(int X, int Y); + // + + // + static string GetCrossingInstruction( + PedestrianSignal signal, bool crossingIsClear) => + (signal, crossingIsClear) switch + { + (PedestrianSignal.Walk, true) => "Cross now", + (PedestrianSignal.Walk, false) => "Wait for the crossing to clear", + _ => "Wait for the walk signal" + }; + + enum PedestrianSignal + { + Stop, + Walk + } + // +} diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/RelationalLogicalPatterns.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/RelationalLogicalPatterns.cs new file mode 100644 index 0000000000000..fa1c6e14dfe8d --- /dev/null +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/RelationalLogicalPatterns.cs @@ -0,0 +1,68 @@ +static class RelationalLogicalPatterns +{ + public static void Run() + { + ShowExpressionAndPattern(-4); + Console.WriteLine( + $"Comfortable temperature: {IsComfortableTemperature(21)}"); + Console.WriteLine($"Weekend: {IsWeekend(DayOfWeek.Saturday)}"); + Console.WriteLine($"Active status: {IsActive(Status.Pending)}"); + Console.WriteLine($"Accepted priority: {IsAcceptedPriority(9)}"); + Console.WriteLine( + $"Heat warning: {GetHeatWarning(36, isOutdoors: true)}"); + } + + // + static void ShowExpressionAndPattern(int temperature) + { + bool freezeWarningFromExpression = temperature < 0; + bool freezeWarningFromPattern = temperature is < 0; + + string description = temperature switch + { + < 0 => "Freezing", + 0 => "Freezing point", + > 0 => "Above freezing" + }; + + Console.WriteLine( + $"Expression: {freezeWarningFromExpression}; " + + $"pattern: {freezeWarningFromPattern}; {description}"); + } + // + + // + static bool IsComfortableTemperature(int temperature) => + temperature is >= 18 and <= 24; + // + + // + static bool IsWeekend(DayOfWeek day) => + day is DayOfWeek.Saturday or DayOfWeek.Sunday; + + static bool IsActive(Status status) => + status is not Status.Complete; + + enum Status + { + Pending, + Running, + Complete + } + // + + // + static bool IsAcceptedPriority(int priority) => + priority is (>= 1 and <= 3) or 9; + // + + // + static string GetHeatWarning(int temperature, bool isOutdoors) => + temperature switch + { + > 35 when isOutdoors => "High heat outdoors", + > 35 => "High heat", + _ => "No heat warning" + }; + // +} diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index e388ba15c7895..2aaa5559099fa 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -121,6 +121,12 @@ items: href: fundamentals/patterns/declaration-constant-var-patterns.md - name: Type patterns href: fundamentals/patterns/type-patterns.md + - name: Property and positional patterns + href: fundamentals/patterns/property-positional-patterns.md + - name: Relational, logical, and parenthesized patterns + href: fundamentals/patterns/relational-logical-patterns.md + - name: List and slice patterns + href: fundamentals/patterns/list-patterns.md - name: Discards and the discard pattern href: fundamentals/patterns/discards.md - name: Expressions and statements