C# List Pattern: Match Collections with Concise Syntax
c# list pattern: Understand C# list patterns, a concise way to match collection length and elements in pattern matching contexts.
List patterns in C# let you match a sequence of elements against a pattern, checking length and element values in a single expression. Introduced in C# 11, they work with any type that has a length or count property, an indexer, and a slice support. This article explains the syntax, practical usage, and edge cases of the C# list pattern, so you can decide when it simplifies your code and when traditional collection checks are clearer.
The Core Syntax of List Patterns
A list pattern is written with square brackets. The simplest form matches an empty sequence: []. To match a specific sequence, list the element patterns inside the brackets, separated by commas. For example, [1, 2, 3] matches a collection of exactly three elements where the first is 1, the second is 2, and the third is 3. Each element can be any pattern, not just a constant. You can use a discard _ to match any single element, or a property pattern to match a specific shape.
int[] numbers = { 1, 2, 3 }; if (numbers is [1, 2, 3]) { Console.WriteLine("Exactly 1, 2, 3"); }
The pattern requires the collection to have a length or count property. Arrays, List<T>, Span<T>, and ReadOnlySpan<T> all satisfy this requirement. For custom types, you need to provide a Length or Count property and an indexer. The compiler translates the pattern into efficient index-based checks, so there is no runtime allocation or enumeration overhead.
Matching Length and Discarding Elements
Often you only care about the first few elements or the overall length. The discard pattern _ matches any single element, and the slice pattern .. matches zero or more elements. The slice pattern is particularly useful for matching the beginning or end of a sequence.
if (numbers is [1, .., 3]) { Console.WriteLine("Starts with 1 and ends with 3"); }
Here .. matches any number of elements in the middle. You can also use .. with a variable to capture the slice: [1, .. var rest] assigns the remaining elements to rest. This works with any type that has a slice method, such as arrays and List<T>. The slice pattern can appear at most once in a list pattern, and it can be placed anywhere.
if (numbers is [.., 2, 3]) { Console.WriteLine("Ends with 2, 3"); }
This matches a sequence that ends with 2 and 3, regardless of how many elements precede them. The slice pattern is a powerful tool for parsing input, such as command-line arguments or protocol messages, where you need to validate a prefix or suffix.
Using List Patterns in Switch Expressions
List patterns shine in switch expressions and switch statements, where you can match different shapes of a collection without writing manual length checks and index accesses. Consider a function that returns a description of a numeric sequence:
static string Describe(int[] numbers) => numbers switch { [] => "Empty", [1] => "Single 1", [1, ..] => "Starts with 1", [_, _] => "Two elements", _ => "Other" };
The switch expression evaluates the patterns in order. The [1, ..] pattern matches any non-empty sequence that starts with 1. The [_, _] pattern matches exactly two elements, regardless of their values. The final _ is the fallback. This approach is more declarative than a series of if statements, and it keeps the logic in one place.
You can also combine list patterns with other patterns, such as type patterns or property patterns. For instance, [var first, ..] captures the first element into a variable, and [int x, int y] matches a two-element sequence where both elements are integers. The compiler enforces that the element patterns are compatible with the collection's element type.
Combining List Patterns with Nested Patterns
List patterns can be nested inside other list patterns, allowing you to match complex structures. For example, a jagged array can be matched with a pattern like [[1, 2], [3, 4]]. Each inner list pattern is itself a list pattern, so the outer pattern matches a collection of two collections, each with exactly two elements. This is useful for validating matrix-like data.
int[][] matrix = { new[] { 1, 2 }, new[] { 3, 4 } }; if (matrix is [[1, 2], [3, 4]]) { Console.WriteLine("2x2 matrix with expected values"); }
You can also use the slice pattern inside nested patterns to match variable-length inner sequences. The same rules apply: each list pattern can have at most one slice. This nesting capability makes list patterns a compact way to express structural constraints that would otherwise require multiple loops and conditionals.
Edge Cases and Limitations
List patterns require the input to be non-null. If you pass null, the pattern fails to match, and you need an explicit null check if that is a valid state. The pattern also requires the collection to have an indexer that can be accessed without throwing. For example, a Dictionary<TKey, TValue> does not have a positional indexer that returns elements in order, so you cannot use list patterns directly on a dictionary. You would need to use dict.ToArray() or another ordered representation.
The slice pattern relies on a Slice method or a Range indexer. Most .NET collections provide this, but custom collections may not. If your type does not support slicing, you cannot use .. in a list pattern. Also, list patterns are evaluated eagerly, meaning they check the length first and then access each index. For very large collections, this is still O(1) for length and O(n) for element checks, but the pattern is designed for small, fixed-size validation rather than complex algorithmic matching.
Another limitation is that list patterns do not work with IEnumerable<T> directly, because that interface does not expose a length or indexer. You would need to materialize the sequence into an array or list first. This is an important distinction when working with lazy sequences: the pattern forces eager evaluation, which may have performance implications if the source is infinite or expensive to enumerate.
Performance and Maintainability Considerations
List patterns compile to efficient index-based code, not to loops or LINQ queries. The compiler generates direct length checks and index accesses, which are as fast as hand-written code. However, the pattern does not avoid the cost of enumerating a lazy sequence if you convert it to an array. In performance-sensitive paths, prefer using Span<T> or ReadOnlySpan<T> with list patterns to avoid allocations.
From a maintainability perspective, list patterns reduce the amount of boilerplate needed to validate collection shapes. Instead of writing multiple if statements with Length checks and index accesses, you express the entire condition in one pattern. This is especially valuable in parsing or validation code where the expected structure is well-defined. However, for complex conditions that involve many arbitrary checks, a traditional loop may be clearer. Use list patterns when the shape is fixed and the pattern is self-documenting.
When to Choose List Patterns Over Traditional Checks
List patterns are the right choice when you need to match a collection's length and element values in a single expression, especially inside a switch or a conditional. They are ideal for parsing command-line arguments, validating fixed-format input, or handling protocol messages. If you need to iterate over a collection and perform different actions based on its content, a foreach loop with conditional logic may be more appropriate, because it allows side effects and early exits. List patterns are purely declarative; they do not produce side effects and are best used for branching.
Consider a scenario where you need to process a list of tokens. A list pattern can quickly identify the shape of the token list and route to the appropriate handler. If the logic requires complex transformations or accumulation, you would likely use a loop instead. The decision comes down to whether the primary concern is structural matching or element-wise processing. For structural matching, list patterns are concise and less error-prone than manual index checks.