📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do list patterns work in C# 11? Explain matching on length and elements, the discard and slice patterns, capturing a slice with .. var rest, which types support list patterns (countable and indexable, plus slicing support), the allocation cost of slices for arrays, lists, strings and spans, and real uses such as command parsing.

List patterns: syntax, slice patterns, type requirements, slice costs and practical uses.

Mid-Level Coding Intermediate csharp-11 list-patterns parsing pattern-matching spans

🎯 Key points

  • List patterns (C# 11) match sequences by length and element patterns
  • Each position accepts any pattern, and patterns can nest property patterns
  • The .. slice pattern matches zero or more elements; only one is allowed
  • Types need Length or Count plus an indexer; capturing a slice needs a Range indexer or Slice method
  • Arrays, List<T>, strings and spans are supported; IEnumerable<T> is not
  • Slices of spans are free views; slices of arrays, lists and strings allocate
  • The compiler checks exhaustiveness using lengths and reports unreachable arms

⏱ 30-second answer

A list pattern (C# 11) matches a sequence by its elements: [] is empty, [var x] has exactly one element, [1, _, 3] has three with given first and last, [var first, ..] has at least one, and [.., var last] captures the last. Each element position takes any pattern (constant, relational, type, var, _). .. is a slice pattern matching zero or more elements, and at most one is allowed; .. var rest captures the slice. A type supports list patterns if it is countable (Length or Count) and indexable (an int or Index indexer); capturing a slice also needs a Range indexer or a Slice method. Arrays, List<T>, strings, Span<T> and ReadOnlySpan<T> work; IEnumerable<T> does not. Slices of spans are free, while slices of arrays, lists and strings allocate copies.

🗣 Full answer (senior)

How I would answer this in an interview

List patterns (C# 11) match a sequence by its length and elements, in is, switch statements and switch expressions: args is ["deploy", var env, ..]. They combine with every other pattern, support a discard (_) for single elements and a slice (..) for zero or more elements, which can be captured (.. var rest). I'd cover matching rules, supported types, slice allocation costs, and real uses like command parsing.

1. Matching on length and elements

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

numbers is [1, 2, 3];          // exact length and values
numbers is [_, _, _];          // any three elements
numbers is [1, ..];            // starts with 1, any length ≥ 1
numbers is [.., 3];            // ends with 3
numbers is [1, .., 3];         // starts with 1, ends with 3
numbers is [] ;                // empty
numbers is [> 0, > 0, ..];     // element patterns can be any pattern
numbers is [var first, .. var middle, var last];   // capture elements and the slice

Element positions accept any pattern — constant, relational, type, property, nested list patterns.

2. Discard and slice

  • _ matches exactly one element (any value).
  • .. matches zero or more elements; at most one slice per list pattern.
  • .. var rest captures the slice; .. [> 0, ..] applies a nested list pattern to the slice.

3. Which types support list patterns

A type works with list patterns if it is:

  • Countable: has an accessible Length or Count property returning int.
  • Indexable: has an indexer taking Index (supports ^1) or an int indexer (the compiler uses implicit Index support).
  • For capturing slices: has a Range indexer or a Slice(int start, int length) method (implicit Range support).

So: arrays, string, List<T>, Span<T>, ReadOnlySpan<T>, ImmutableArray<T>, and your own types with those members. IEnumerable<T> doesn't work (not countable/indexable). List<T> supports element matching but not slice capture (no Slice method; matching .. without capturing is fine).

4. Allocation cost of slices

Capturing a slice calls the type's range indexer or Slice:

Type .. var rest result Allocates?
T[] New array (RuntimeHelpers.GetSubArray) Yes — copy
string Substring Yes — new string
Span<T> / ReadOnlySpan<T> Slice view No
ImmutableArray<T> New immutable array Yes
List<T> Not supported for capture —
ReadOnlyMemory<T> Slice view No

Unused slices (.. without var) don't allocate. In hot paths, match on spans (args.AsSpan(), line.AsSpan()) to get free slices.

5. Real uses

Command parsing:

static Task<int> RunAsync(string[] args) => args switch
{
    [] or ["help"]                           => ShowHelpAsync(),
    ["deploy", var env]                      => DeployAsync(env, dryRun: false),
    ["deploy", var env, "--dry-run"]         => DeployAsync(env, dryRun: true),
    ["migrate", "up", .. var options]        => MigrateAsync(options),
    ["report", var from, var to] when DateOnly.TryParse(from, out _) => ReportAsync(from, to),
    _                                         => UnknownAsync(args)
};

Binary formats / magic numbers:

static string Detect(ReadOnlySpan<byte> header) => header switch
{
    [0x89, 0x50, 0x4E, 0x47, ..] => "image/png",
    [0xFF, 0xD8, 0xFF, ..]       => "image/jpeg",
    [0x25, 0x50, 0x44, 0x46, ..] => "application/pdf",   // %PDF
    _                            => "application/octet-stream"
};

CSV/fixed fields, routing segments, protocol messages:

var parts = line.Split(',');
if (parts is [var sku, var qty, var price] && int.TryParse(qty, out var q)) { ... }

Recursion on sequences (functional style): int Sum(ReadOnlySpan<int> s) => s is [var head, .. var tail] ? head + Sum(tail) : 0; (fine for spans; would allocate per call with arrays).

6. A production example

A CLI tool for deployment had nested if (args.Length > 1 && args[0] == "deploy" && ...) checks, with off-by-one bugs causing IndexOutOfRangeException for unusual argument counts. Rewriting the dispatcher as a switch expression with list patterns made every supported shape explicit, removed index arithmetic, and the discard arm produced a clear usage message. A file-upload validator used list patterns over ReadOnlySpan<byte> for magic-number detection, rejecting renamed executables before scanning.

🔍 What the interviewer is checking

The interviewer checks you can use list patterns correctly and know their requirements and costs.

Green flags

  • Writes correct patterns for empty, exact, prefix and suffix shapes.
  • Explains slice patterns and the one-slice rule.
  • Knows the countable/indexable/sliceable requirements.
  • Mentions that span slices are free while array and string slices allocate.

Red flags

  • Tries to use them on IEnumerable<T>.
  • Captures slices in hot paths without noticing allocations.
  • Puts general arms before specific ones.

📚 Learn in depth

1. Lowering

args is ["deploy", var env]
// becomes roughly
args is not null && args.Length == 2 && args[0] == "deploy" && (env = args[1]) is var _
span is [var first, .. var rest]
// becomes
span.Length >= 1 && (first = span[0]) is var _ && (rest = span[1..]) is var _

The compiler checks length once, then indexes. ^n indexing uses Length - n. Slices call Slice(start, length) or the range indexer.

2. Length check optimisation

With multiple arms, the decision DAG reads Length once and branches on length ranges, then tests elements — efficient even with many patterns.

3. Interaction with other patterns

order is { Lines: [{ Quantity: > 10 }, ..] }        // first line has quantity > 10
points is [(0, 0), .., (var x, var y)]             // positional patterns inside list patterns
words is ["Dear", .. var middle, "Regards"]

4. Null and empty

List patterns don't match null (they imply non-null). [] matches empty but not null. Use null or [] to match both.

5. Performance guidance

  • Prefer ReadOnlySpan<T> inputs for parsing (no allocation for slices).
  • Avoid capturing slices of arrays/strings in tight loops.
  • For List<T>, use CollectionsMarshal.AsSpan(list) to get span semantics.
  • string.Split allocates; for hot parsing use span-based splitting then list patterns over spans of ranges.

Try it yourself

  1. Write a CLI dispatcher with list patterns and test edge cases (empty, extra args).
  2. Capture a slice of an array, a string and a span; measure allocations with [MemoryDiagnoser].
  3. Detect file types with list patterns over a span.
  4. Try list patterns on IEnumerable<T> and read the error.

💻 Code

public enum OrderStatus { Pending, Paid, Shipped }
public sealed record Order(int Id, OrderStatus Status, decimal Total);

public static class Commands
{
    // Parse simple CLI commands; span input means slices are views, not copies
    public static string Run(ReadOnlySpan<string> args) => args switch
    {
        [] => "usage: tool <command> [options]",
        ["help"] or ["--help"] or ["-h"] => "commands: deploy, rollback, status",
        ["deploy", var env] => $"deploying to {env}",
        ["deploy", var env, "--canary", var percent] when int.TryParse(percent, out var p) && p is > 0 and <= 100
            => $"canary {p}% to {env}",
        ["rollback", var env, .. var rest] => $"rolling back {env} with {rest.Length} extra option(s)",
        ["status", ..] => "all systems normal",
        [var unknown, ..] => $"unknown command '{unknown}'",
    };

    public static string Review(IReadOnlyList<Order> orders) => orders switch
    {
        [] => "no orders",
        [{ Status: OrderStatus.Pending }] => "one pending order",
        [.., { Total: > 1000m } last] => $"latest order {last.Id} is high value",
        [{ Status: OrderStatus.Paid }, _, ..] => "first order paid, more to review",
        _ => "review manually",
    };

    // Byte framing: STX payload ETX, no allocation
    public static bool TryReadFrame(ReadOnlySpan<byte> data, out ReadOnlySpan<byte> payload)
    {
        if (data is [0x02, .. var body, 0x03])
        {
            payload = body;
            return true;
        }
        payload = default;
        return false;
    }
}

public static class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine(Commands.Run(["deploy", "prod", "--canary", "10"]));
        Console.WriteLine(Commands.Run(["rollback", "test", "--force", "--quiet"]));
        Console.WriteLine(Commands.Run(args));

        List<Order> orders = [new(1, OrderStatus.Paid, 50m), new(2, OrderStatus.Pending, 1500m)];
        Console.WriteLine(Commands.Review(orders));   // latest order 2 is high value

        ReadOnlySpan<byte> frame = [0x02, 0x41, 0x42, 0x03];
        if (Commands.TryReadFrame(frame, out var payload))
            Console.WriteLine(payload.Length);         // 2
    }
}

⚠ Common mistakes

1. Using list patterns on IEnumerable<T>

What happens: sequence is [var a, ..] on a LINQ query. Why it is wrong: not countable/indexable — compile error. Do instead: materialise to an array/list or use spans.

2. Capturing slices of arrays in hot paths

What happens: .. var rest on string[] per request. Why it is wrong: allocates a new array each time. Do instead: match on spans or avoid capturing.

3. Expecting slice capture on List<T>

What happens: list is [_, .. var rest]. Why it is wrong: List<T> has no Slice/range indexer. Do instead: use CollectionsMarshal.AsSpan(list) or arrays.

4. Multiple slices

What happens: [.., 1, ..]. Why it is wrong: only one slice allowed. Do instead: use Contains/IndexOf for "anywhere" searches.

5. Forgetting null

What happens: expecting [] to match null. Why it is wrong: list patterns imply non-null. Do instead: null or [].

6. Over-complex patterns

What happens: deeply nested list and property patterns in one arm. Why it is wrong: hard to read. Do instead: split arms or extract methods.

7. Using list patterns where indexes are clearer

What happens: forcing patterns for simple args.Length == 0 checks. Why it is wrong: no benefit. Do instead: use patterns where shapes vary.

8. Recursion with array slices

What happens: recursive functions slicing arrays via list patterns. Why it is wrong: O(n²) copying. Do instead: use spans or indices.

🏛 Senior insight: trade-offs

List patterns are at their best when the shape of the data is the logic: commands, routes, protocol frames and small sequences of states. They make that logic declarative and let the compiler check exhaustiveness and unreachable arms.

Situation Good fit?
Parsing small, fixed-shape inputs yes
Byte protocol framing on spans yes, allocation-free
Searching large collections no, use loops or LINQ
Complex grammars no, use a real parser

The senior detail is cost awareness: the same pattern is free on a span and allocates on an array or string. Choosing span inputs for parsers keeps the readability without the garbage.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate combines list and property patterns. Why: List patterns are one family within pattern matching.
    How to answer

    C# patterns now cover most data checks. There are type, constant and null patterns, relational ones like >= 100, logical and, or and not combinators, and property patterns, including extended ones like { Customer.Address.State: "NSW" }. There are also positional patterns that use Deconstruct, var and discard patterns, and list patterns like [first, .., last]. The compiler turns a switch into an efficient decision tree, warns when it isn't exhaustive, and errors on unreachable cases. I use patterns for closed sets of data shapes and polymorphism when new types keep being added.

  2. When: Candidate writes a switch over arguments. Why: Switch expressions host most list patterns.
    How to answer

    A switch expression returns a value: status switch { Paid => "Paid", _ => "Other" }. Each arm is a pattern, an optional when guard, and an expression. There's no fall-through or break, and the first matching arm wins. The result type comes from the arms or from the target. The compiler warns with CS8509 if it isn't exhaustive, and an unmatched value throws SwitchExpressionException. Switching on a tuple like (state, trigger) gives very readable state machines. When an arm needs several statements or side effects, a classic switch statement or polymorphism is clearer.

  3. When: Candidate mentions ReadOnlySpan input. Why: Span slicing explains why some slices are free.
    How to answer

    ReadOnlySpan<char> is a view over contiguous characters, which can be a string, an array or a stack buffer, made of a reference plus a length. Slicing it is free (no new string), and almost every parsing API now takes spans, so I can split a CSV line, trim fields and parse numbers and dates with zero allocations until I really need a string. It's a ref struct, so it can't be stored in a class field, boxed, captured by a lambda or held across an await. For those cases I use ReadOnlyMemory<char> and call .Span when processing. I reach for it in measured hot paths such as parsers, protocol code and high-volume ingestion.

🛠 Real-world problems: how you handle them

Brittle index checks in a deployment bot replaced with list patterns
Situation
A chat-ops bot for [deployments] parsed commands with nested if statements on args.Length and args[i].
Signals
Several IndexOutOfRangeException incidents when engineers typed partial commands, and a dangerous case where `deploy` with no environment defaulted to production.
Investigation
Code review showed duplicated and inconsistent length checks across [14] command handlers.
Resolution
Rewrote parsing as one switch expression with list patterns over ReadOnlySpan<string>, with an explicit arm for every valid shape and a usage message for the rest.
Prevention
Unit tests for each pattern arm, and the compiler's unreachable-arm errors caught mistakes when new commands were added.

How to tell it (STAR)

**Situation:** our [chat-ops bot] crashed on partial commands and once nearly deployed to production because of a missing argument. **Task:** I volunteered to make the parsing safe. **Action:** I replaced the index checks with a single switch expression using list patterns, one arm per valid command shape, and made the fallback a usage message. **Result:** crashes stopped, the dangerous default was gone, and adding a new command became [one line plus a test].

↩ Usually asked after

More in Modern C#

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me