Pattern Matching

Patterns test a value's shape and extract parts of it, in is expressions, switch statements and switch expressions.

Concept

Patterns test a value's shape and extract parts of it, in is expressions, switch statements and switch expressions.

Type (declaration) An int, binding it to n 0, null, "VCU" An equal constant and, or, not Combinations: >= 0 and < 0.15 { Length: <= 3 }, { Id: 0x7DF }

Objects whose properties match (nested: { Data.Length: 0 }) CanFrame(0x7DF, _) Deconstructable types (records)

[], [var x], [1,.., var last], [_, 0x10,..] Arrays/lists by elements; .. is a slice int n when n > 0

The compiler checks switch expressions for exhaustiveness (warning if some input is not handled) and for arms that can never match (error).

Why It Matters

Pattern matching turns long if/cast chains into declarative, readable decision tables — perfect for decoding protocol messages, classifying inputs and handling results.

Syntax

if (obj is Car { Doors: 4 } car) { ... }
var r = value switch { int n when n > 0 => "pos", null => "null", _ => "other" };
if (frame is { Data: [_, 0x7F, var sid, var nrc] }) { ... }

Example

object[] inputs = [42, -7, 0, 3.5, "ECU", null!, new[] { 1, 2, 3 }, new CanFrame(0x7DF, [0x02, 0x10, 0x03])];

foreach (object value in inputs)
    Console.WriteLine($"{Show(value),-22} -> {Classify(value)}");

// Property and positional patterns on records
var frames = new[]
{
    new CanFrame(0x7DF, [0x02, 0x10, 0x03]),
    new CanFrame(0x7E8, [0x03, 0x7F, 0x22, 0x31]),
    new CanFrame(0x7E8, [0x06, 0x62, 0xF1, 0x90, 0x41, 0x55]),
    new CanFrame(0x100, []),
};
foreach (var f in frames)
    Console.WriteLine($"0x{f.Id:X3}: {Decode(f)}");

// List patterns (C# 11)
int[][] lists = [[], [1], [1, 2], [1, 2, 3, 4], [9, 8, 7]];
foreach (var l in lists)
    Console.WriteLine($"[{string.Join(",", l)}] -> {DescribeList(l)}");

// Relational and logical patterns in 'is'
double soc = 0.12;
if (soc is >= 0 and < 0.15) Console.WriteLine($"SoC {soc:P0}: critical");
Console.WriteLine($"is not null and not 0: {inputs[0] is not null and not 0}");

static string Classify(object? value) => value switch
{
    null => "null (constant pattern)",
    int n when n > 0 => "positive int (type pattern + guard)",
    int n when n < 0 => "negative int",
    0 => "zero (constant pattern)",
    double d and 
…

Expected Result

The UDS responses are decoded purely by patterns: a negative response (0x7F) exposes the service ID and NRC as variables, and a "ReadDataByIdentifier" response for DID F190 (VIN) slices the remaining bytes.

42                     -> positive int (type pattern + guard)
-7                     -> negative int
0                      -> zero (constant pattern)
3.5                    -> small double 3.5 (relational + logical)
ECU                    -> short string 'ECU' (property pattern)
null                   -> null (constant pattern)
int[]{1,2,3}           -> int[] of length 3
CanFrame { Id = 2015, Data = System.Byte[] } -> OBD broadcast request (positional pattern)
0x7DF: DiagnosticSessionControl, session 0x03
0x7E8: negative response to 0x22, NRC 0x31
0x7E8: VIN part: AU
0x100: empty frame
[] -> empty
[1] -> single 1
[1,2] -> exactly 1, 2
[1,2,3,4] -> starts with 1, ends with 4
[9,8,7] -> starts with 9, 1 in the middle
SoC 12%: critical
is not null and not 0: True

Common Mistakes

Putting a general arm (int n) before a specific one (0) — the compiler reports the later arm as unreachable (error CS8510).

Forgetting the discard arm and getting SwitchExpressionException at run time for unhandled inputs (the compiler warns with CS8509).

Using is with a type pattern but then casting again.

Writing very long switch expressions mixing unrelated concerns.

Best Practices

Order arms from most specific to most general.

Use property and list patterns to decode structured data instead of indexing manually.

Keep each arm's result simple; extract methods for complex handling.

Use is not null for null checks.

Interview Relevance

Medium–high: "what pattern types exist?", "switch statement vs switch expression", "what are list patterns?", "how do records work with positional patterns?".

Practice Questions

Classify a Shape hierarchy (circle, rectangle, square) by area using type and property patterns.

Decode OBD-II PID responses [0x41, pid, a, b] with list patterns (RPM = (256a + b)/4).

Write a switch expression that maps (bool ignition, double speed) tuples to a vehicle state.

Match strings that start with "P0" and have length 5 using is { Length: 5 } and StartsWith.

Quick Quiz

Q1. (Multiple choice) Which pattern matches an array whose first element is 1? A) { 1 } B) [1,..] C) (1, _) D) int 1 Answer: B.

Q2. (Predict the output) 3.5 switch { double d and (> 0 and < 10) => "small", _ => "other" } Answer: small (verified).

Q3. (True/False) A switch expression without a discard arm always fails to compile.

Answer: False. The compiler warns (CS8509) if it is not exhaustive; an unmatched input throws at run time.

Q4. (Predict the output) What does DescribeList([9, 8, 7]) return?

Answer: starts with 9, 1 in the middle (verified).

Coding Challenge

Write a switch expression that decodes an OBD-II response byte[] of the form [0x41, 0x0C, a, b] into "RPM = n" and [0x41, 0x0D, speed] into "Speed = n km/h", otherwise "unsupported".

static string DecodeObd(byte[] r) => r switch
{
    [0x41, 0x0C, var a, var b] => $"RPM = {(256 * a + b) / 4}",
    [0x41, 0x0D, var speed] => $"Speed = {speed} km/h",
    _ => "unsupported"
};
Console.WriteLine(DecodeObd([0x41, 0x0C, 0x1A, 0xF8]));
Console.WriteLine(DecodeObd([0x41, 0x0D, 0x50]));
Console.WriteLine(DecodeObd([0x7F, 0x01, 0x12]));