Skip to main content

Pattern Matching

Definition

Pattern matching is a mechanism for testing values against shapes (patterns) and extracting information from them. Introduced in C# 7 and significantly expanded in subsequent versions, pattern matching replaces verbose if-else chains and switch statements with concise, expressive syntax that combines type checking, value comparison, and data extraction into a single construct.

Core Concepts

The is Expression

The is operator checks whether an expression matches a pattern. It can test types, declare variables, and combine with relational/logical operators.

// Type pattern — test and cast
if (obj is string s)
Console.WriteLine(s.Length);

// Declaration pattern — extract value
if (value is int number && number > 0)
Console.WriteLine($"Positive: {number}");

// Negation pattern
if (value is not null)
Console.WriteLine(value);

Switch Expressions (C# 8+)

Switch expressions are a concise, expression-based form of switch that returns a value. Every arm is a pattern followed by => and a result.

string Classify(int value) => value switch
{
< 0 => "Negative",
0 => "Zero",
> 0 and <= 100 => "Small positive",
> 100 => "Large positive",
};

Type Patterns

Match based on the runtime type of an object and optionally bind a variable:

string Describe(object shape) => shape switch
{
Circle c => $"Circle with radius {c.Radius}",
Rectangle r => $"Rectangle {r.Width}x{r.Height}",
Triangle => "A triangle",
null => "No shape",
_ => "Unknown shape"
};

Property Patterns

Match against properties of an object using { Property: pattern } syntax:

decimal CalculateDiscount(Order order) => order switch
{
{ Customer.IsVip: true, Total: > 1000m } => 0.20m,
{ Customer.IsVip: true } => 0.10m,
{ Total: > 500m } => 0.05m,
_ => 0m
};

C# 10 extended property patterns to allow nested access without repeating braces:

// C# 10 extended property pattern
if (order is { Customer.IsVip: true, Items.Count: > 5 })
ApplyBulkDiscount(order);

Positional Patterns

Work with types that support deconstruction (via Deconstruct method or positional records):

record Point(double X, double Y);

string Classify(Point p) => p switch
{
(0, 0) => "Origin",
(0, _) => "On Y-axis",
(_, 0) => "On X-axis",
( > 0, > 0) => "First quadrant",
_ => "Elsewhere"
};

Relational Patterns

Compare against constant values using <, >, <=, >=:

string GetGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};

Logical Patterns

Combine patterns with and, or, not:

bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

bool IsNotValid(int? age) => age is null or < 0 or > 150;

List Patterns (C# 11)

Match elements within arrays and lists. Use [..] for slice patterns:

int SumFirstTwo(int[] numbers) => numbers switch
{
[] => 0,
[var a] => a,
[var a, var b] => a + b,
[var a, var b, ..] => a + b,
};

string ClassifyResponse(int[] codes) => codes switch
{
[200] => "OK",
[200, 200] => "All OK",
[400, ..] => "Client error prefix",
[>= 500] => "Server error",
_ => "Unknown"
};

Code Examples

State Machine with Pattern Matching

enum State { Open, Closed, Locked }
enum Action { Open, Close, Lock, Unlock }

State Transition(State current, Action action) => (current, action) switch
{
(State.Open, Action.Close) => State.Closed,
(State.Closed, Action.Open) => State.Open,
(State.Closed, Action.Lock) => State.Locked,
(State.Locked, Action.Unlock) => State.Closed,
_ => current // No transition
};

Data Validation

static string ValidateUser(User user) => user switch
{
null => "User is required",
{ Name: null or "" } => "Name is required",
{ Age: < 0 or > 150 } => "Invalid age",
{ Email: not string { Length: > 0 } } => "Email is required",
_ => "Valid"
};

When to Use

  • Replacing if-else chains — switch expressions are more readable when branching on the same value
  • Type checking and castingis Type var is safer and cleaner than typeof + cast
  • Data validation — property patterns express structural constraints clearly
  • State machines — tuple patterns on (state, event) pairs make transitions declarative
  • Decomposing records and tuples — positional patterns align naturally with record types
  • Handling null casesis null, is not null integrate with all other patterns

Common Pitfalls

Pattern order matters

Patterns are evaluated top to bottom. More specific patterns must appear before more general ones, or they will never be reached. The compiler warns about unreachable patterns, but only within the same switch expression.

Exhaustiveness

Switch expressions must be exhaustive — every possible input must have a matching arm. Use the discard pattern _ as a catch-all when needed. The compiler will error if arms are missing.

Performance in hot paths

Complex pattern matching with nested property patterns or list patterns involves runtime type checks and allocations. In performance-critical loops, benchmark before adopting. Simple type and constant patterns are JIT-friendly.

Use compiler warnings

Enable <TreatWarningsAsErrors>true</TreatWarningsAsErrors> or at least CS8509 (non-exhaustive switch) to catch missing pattern arms at compile time.

Key Takeaways

  1. Pattern matching unifies type checking, value comparison, and data extraction into one syntax.
  2. Switch expressions return a value and are more concise than switch statements.
  3. Property and positional patterns enable structural matching without manual casts.
  4. Relational and logical patterns (and, or, not) replace compound if conditions.
  5. List patterns (C# 11) bring destructuring to arrays and spans.
  6. Always order patterns from most specific to most general.

Interview Questions

Q: What is pattern matching in C#? Pattern matching is a feature that lets you test an expression against a series of patterns and extract data when matched. It extends the is operator and switch construct to support type patterns, property patterns, positional patterns, relational patterns, logical patterns, and list patterns.

Q: What are switch expressions and how do they differ from switch statements? Switch expressions (C# 8) are expression-based: they return a value, use => arms, and require exhaustiveness. Switch statements are statement-based, use case/break, and do not return a value. Switch expressions are preferred when the goal is to compute a value.

Q: What new patterns were added in C# 11? C# 11 introduced list patterns for matching sequences ([a, b, .., c]), slice patterns (..) within list patterns, and enhanced the Span<char> pattern matching for parsing scenarios.

Q: What is the difference between is Type and as Type? is Type returns a boolean and, with declaration patterns, also binds a typed variable. as returns the casted value or null if the cast fails. is is preferred with pattern matching because it combines the check and the binding.

Q: What is a property pattern and when would you use it? A property pattern ({ Prop: value }) matches an object whose properties satisfy nested patterns. Use it when you need to branch based on the shape of data — for example, routing requests based on HTTP method and path, or validating DTOs.

References