C# List Patterns: Match Arrays and Collections

List patterns, introduced in C# 11, extend pattern matching to sequences such as arrays, lists, and spans. This article covers how list patterns work, their core syntax elements—including exact matching, discard patterns, and slice patterns—and practical examples demonstrating how to match and deconstruct collections efficiently using is expressions and switch statements.

What Are List Patterns?

List patterns allow you to test whether a collection matches a specific sequence of elements and structure. They work with any type that is countable (has a Length or Count property) and indexable (provides an integer indexer), including T[], List<T>, Span<T>, and ReadOnlySpan<T>.

A list pattern is enclosed in square brackets [...]. The pattern checks both the length of the collection and the values or patterns of its individual elements.

Basic Sequence and Element Matching

The simplest list pattern matches elements in exact sequence and checks the exact length of the collection:

int[] numbers = { 1, 2, 3 };

if (numbers is [1, 2, 3])
{
    Console.WriteLine("Matches exact values and length of 3.");
}

If the array has fewer or more elements, the pattern fails.

Discard Pattern (_)

To match an element regardless of its value while enforcing collection length, use the discard pattern _:

int[] scores = { 10, 42, 30 };

if (scores is [10, _, 30])
{
    Console.WriteLine("Starts with 10, ends with 30, and has exactly 3 items.");
}

The Slice Pattern (..)

The slice pattern (..) matches zero or more elements. It allows you to inspect specific positions without restricting the collection to a fixed size.

Matching Prefixes and Suffixes

int[] values = { 1, 2, 3, 4, 5 };

// Matches any collection that starts with 1
bool startsWithOne = values is [1, ..];

// Matches any collection that ends with 5
bool endsWithFive = values is [.., 5];

// Matches any collection starting with 1 and ending with 5
bool startsOneEndsFive = values is [1, .., 5];

Only one slice pattern (..) is permitted within a single list pattern.

Capturing Slices

You can capture the elements matched by a slice pattern using a variable declaration:

string[] pathSegments = { "api", "v1", "users", "123" };

if (pathSegments is ["api", var version, .. var remaining])
{
    Console.WriteLine($"Version: {version}"); // "v1"
    Console.WriteLine($"Remaining: {string.Join('/', remaining)}"); // "users/123"
}

The captured sub-sequence retains the collection's compatible slice type (for example, an array slice yields an array, while a span slice yields a sub-span).

Combining with Relational and Logical Patterns

Elements inside list patterns can use relational (<, >, <=, >=) and logical (and, or, not) patterns:

int[] measurements = { 5, 12, 100 };

if (measurements is [> 0, >= 10 and <= 20, not 0])
{
    Console.WriteLine("All three elements satisfy their respective conditions.");
}

Practical Application: Command-Line Argument Parsing

List patterns are particularly effective inside switch expressions for command-line parsing or message handling:

string HandleCommand(string[] args) => args switch
{
    ["run"]                      => "Executing default run...",
    ["run", "--profile", var p]  => $"Running with profile: {p}",
    ["config", "set", var k, var v] => $"Setting {k} to {v}",
    ["help" or "-h" or "--help"] => "Displaying help...",
    []                           => "No arguments provided.",
    [var cmd, ..]                => $"Unknown command: {cmd}"
};

This syntax replaces multiple nested length checks, null validations, and index boundary checks with declarative, type-safe constructs evaluated by the compiler.