C# Pattern Matching

Pattern matching lets you test a value against a shape, type, or condition and extract information from it — all in one concise expression. It makes complex conditional code shorter, cleaner, and easier to read.

Why Pattern Matching?

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT pattern matching:                                   │
│                                                              │
│  if (obj is string)                                          │
│  {                                                           │
│      string s = (string)obj;   // cast separately            │
│      Console.WriteLine(s.Length);                            │
│  }                                                           │
│                                                              │
│  WITH pattern matching:                                      │
│                                                              │
│  if (obj is string s)          // test AND cast in one step  │
│  {                                                           │
│      Console.WriteLine(s.Length);                            │
│  }                                                           │
└──────────────────────────────────────────────────────────────┘

Type Pattern — is keyword

The is keyword checks the type and, if matched, assigns the value to a new variable in one step.

object[] items = { "hello", 42, 3.14, true, "world" };

foreach (object item in items)
{
    if (item is string text)
        Console.WriteLine("String: " + text.ToUpper());
    else if (item is int number)
        Console.WriteLine("Int: " + (number * 2));
    else if (item is double d)
        Console.WriteLine("Double: " + d);
    else if (item is bool flag)
        Console.WriteLine("Bool: " + flag);
}
// String: HELLO
// Int: 84
// Double: 3.14
// Bool: True
// String: WORLD

Switch Expression with Patterns (C# 8+)

Switch expressions return a value based on pattern matching. They replace verbose switch statements with a concise arrow syntax.

static string Classify(object obj) => obj switch
{
    int n when n < 0    => "Negative integer",
    int n when n == 0   => "Zero",
    int n               => "Positive integer: " + n,
    string s            => "String of length " + s.Length,
    bool b              => "Boolean: " + b,
    null                => "Null value",
    _                   => "Unknown type"   // _ = default case
};

Console.WriteLine(Classify(-5));        // Negative integer
Console.WriteLine(Classify(0));         // Zero
Console.WriteLine(Classify(42));        // Positive integer: 42
Console.WriteLine(Classify("hello"));   // String of length 5
Console.WriteLine(Classify(null));      // Null value
Console.WriteLine(Classify(3.14));      // Unknown type

Switch Expression Diagram

┌──────────────────────────────────────────────────────────────┐
│  obj switch                                                  │
│  {                                                           │
│      pattern1  =>  result1,   ← checked first                │
│      pattern2  =>  result2,   ← checked second               │
│      pattern3  =>  result3,   ← checked third                │
│      _         =>  default    ← fallback if none match       │
│  }                                                           │
│                                                              │
│  Returns the value of the first matching arm                 │
└──────────────────────────────────────────────────────────────┘

Property Pattern

Property patterns match based on the values of an object's properties — no need to extract values manually first.

class Order
{
    public string Status { get; set; }
    public double Total  { get; set; }
}

static string GetLabel(Order o) => o switch
{
    { Status: "Cancelled" }                    => "❌ Cancelled",
    { Status: "Shipped",   Total: > 1000 }    => "🚀 Priority Shipment",
    { Status: "Shipped" }                      => "📦 Shipped",
    { Status: "Pending",   Total: > 500 }     => "⏳ Large Pending",
    { Status: "Pending" }                      => "⏳ Pending",
    _                                          => "❓ Unknown"
};

Console.WriteLine(GetLabel(new Order { Status = "Shipped",  Total = 1500 }));
// 🚀 Priority Shipment

Console.WriteLine(GetLabel(new Order { Status = "Pending",  Total = 200 }));
// ⏳ Pending

Console.WriteLine(GetLabel(new Order { Status = "Cancelled", Total = 50 }));
// ❌ Cancelled

Relational Patterns (C# 9+)

Relational patterns use <, >, <=, >= directly in switch arms.

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

Console.WriteLine(GetGrade(95));   // A
Console.WriteLine(GetGrade(82));   // B
Console.WriteLine(GetGrade(55));   // F

Logical Patterns: and, or, not (C# 9+)

Patterns can be combined using and, or, and not.

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

Console.WriteLine(Classify(-3));    // Negative
Console.WriteLine(Classify(0));     // Zero
Console.WriteLine(Classify(7));     // Small positive
Console.WriteLine(Classify(50));    // Medium positive
Console.WriteLine(Classify(999));   // Large positive

// 'not' pattern:
bool IsNotNull(object obj) => obj is not null;
Console.WriteLine(IsNotNull("hello"));   // True
Console.WriteLine(IsNotNull(null));      // False

Tuple Pattern

Tuple patterns match multiple values at once inside a switch expression.

static string GetTrafficLight(string color, bool isDay) => (color, isDay) switch
{
    ("Red",    _    ) => "Stop",
    ("Green",  true ) => "Go — daytime",
    ("Green",  false) => "Go — but caution at night",
    ("Yellow", _    ) => "Slow down",
    _                 => "Unknown signal"
};

Console.WriteLine(GetTrafficLight("Red",    true));    // Stop
Console.WriteLine(GetTrafficLight("Green",  false));   // Go — but caution at night
Console.WriteLine(GetTrafficLight("Yellow", true));    // Slow down

Tuple Pattern Diagram

┌──────────────────────────────────────────────────────────────┐
│  (color, isDay) switch                                       │
│                                                              │
│  ("Red",    _    ) → match Red, any day value                │
│  ("Green",  true ) → match Green AND daytime                 │
│  ("Green",  false) → match Green AND nighttime               │
│  _                 → match everything else                   │
└──────────────────────────────────────────────────────────────┘

List Pattern (C# 11+)

List patterns match elements at specific positions in an array or list.

int[] arr1 = { 1, 2, 3 };
int[] arr2 = { 1, 2, 99 };
int[] arr3 = { 1 };
int[] arr4 = { };

static string Describe(int[] arr) => arr switch
{
    []           => "Empty array",
    [var x]      => "One element: " + x,
    [1, 2, 3]    => "Exactly 1, 2, 3",
    [1, 2, ..]   => "Starts with 1, 2",
    _            => "Other"
};

Console.WriteLine(Describe(arr1));   // Exactly 1, 2, 3
Console.WriteLine(Describe(arr2));   // Starts with 1, 2
Console.WriteLine(Describe(arr3));   // One element: 1
Console.WriteLine(Describe(arr4));   // Empty array

Pattern Matching with OOP

abstract class Shape { }
class Circle    : Shape { public double Radius; }
class Rectangle : Shape { public double Width, Height; }
class Triangle  : Shape { public double Base, Height; }

static double GetArea(Shape shape) => shape switch
{
    Circle    c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle  t => 0.5 * t.Base * t.Height,
    _            => throw new ArgumentException("Unknown shape")
};

Console.WriteLine(GetArea(new Circle    { Radius = 5 }));         // 78.54
Console.WriteLine(GetArea(new Rectangle { Width = 4, Height = 6 })); // 24
Console.WriteLine(GetArea(new Triangle  { Base = 3, Height = 8 }));  // 12

Pattern Matching Quick Reference

┌────────────────────────────────────────┬──────────────────────────────────────┐
│ Pattern                                │ Example                              │
├────────────────────────────────────────┼──────────────────────────────────────┤
│ Type pattern                           │ obj is string s                      │
│ Constant pattern                       │ x is 42  or  x switch { 0 => ...}    │
│ Relational pattern                     │ n switch { >= 90 => "A" }            │
│ Logical: and, or, not                  │ > 0 and <= 100                       │
│ Property pattern                       │ { Status: "Active", Age: > 18 }      │
│ Tuple pattern                          │ (x, y) switch { (0,0) => "origin" }  │
│ List pattern                           │ arr switch { [1,2,..] => ... }       │
│ Discard pattern                        │ _ => "default"                       │
└────────────────────────────────────────┴──────────────────────────────────────┘

Pattern matching transforms complex chains of if-else and type-checking code into clean, readable expressions. Once you learn it, you will reach for it constantly — especially when working with inheritance hierarchies, union-like data, and multi-condition logic that previously required many nested blocks.

Leave a Comment

Your email address will not be published. Required fields are marked *