← Back to C# series
✨
Modern C#
Modern C# Β· Prerequisite: control flow/classes

21. Pattern Matching

Pattern matching is a powerful tool for branching by the shape of values. Cover is, switch expressions, property patterns, relational patterns, logical patterns, and tuple patterns.

C#.NET 8pattern matchingswitch
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Control flow + classes
OUTCOME
Pattern matching is a powerful tool for branching by the shape of values. Cover is, switch expressions, property patterns, relational patterns, logical patterns, and tuple patterns.

What you'll learn

  • 1Use the `is` pattern for **type check + variable declaration** in one step
  • 2Fluently use **type / property / tuple / list patterns** in `switch` expressions
  • 3Add extra conditions with `when`
  • 4See how pattern matching raises both readability and safety

Overview

C#'s **pattern matching** collapses tall `if/else` ladders and lets the **shape** of data show up directly in code. .NET 8 has nearly every pattern: type, property, tuple, positional, and list.

Core Concepts

1) `is` pattern β€” type + variable

csharp
if (obj is string s)
{
    Console.WriteLine(s.Length);   // s is already cast to string
}

Replaces the old `if (obj is string) { string s = (string)obj; ... }` boilerplate. Combines with `is not`, `and`, `or`.

csharp
if (n is int x and > 0) ...
if (c is 'a' or 'A') ...

2) `switch` expression β€” returns a value

It's an expression β€” assignable directly to a variable. The `default` slot uses `_` (discard).

csharp
string label = day switch
{
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    _ => "other",
};

3) Type pattern β€” inheritance/interface branching

csharp
string sound = animal switch
{
    Dog d  => $"{d.Name} woof",
    Cat c  => $"{c.Name} meow",
    _      => "quiet...",
};

4) Property pattern β€” check object properties

csharp
string zone = point switch
{
    { X: 0, Y: 0 }     => "origin",
    { X: 0 }           => "Y-axis",
    { Y: 0 }           => "X-axis",
    { X: > 0, Y: > 0 } => "Q1",
    _                  => "other",
};

Comparison operators (`>`, `<=` ...) work directly.

5) Tuple pattern β€” multiple values at once

csharp
(int x, int y) p = (1, 2);
string s = (x, y) switch
{
    (0, 0)                     => "origin",
    var (a, b) when a == b     => "diagonal",
    _                          => "other",
};

6) List pattern (.NET 7+) β€” length + position

csharp
int[] arr = [1, 2, 3];
string s = arr switch
{
    []                       => "empty",
    [var only]               => $"single: {only}",
    [1, .., 3]               => "starts with 1, ends with 3",
    [_, _, _]                => "exactly three",
    [var first, .. var rest] => $"first {first}, {rest.Length} remaining",
};

`..` is "whatever in between"; `var name` captures the slice/element into a variable.

7) `when` clause β€” extra conditions

csharp
string s = age switch
{
    int a when a < 0   => "invalid",
    int a when a < 20  => "teen",
    _                  => "adult",
};

Examples

Example 1 β€” `IsPattern`: `is` pattern

csharp
foreach (object item in items)
{
    if (item is string s)        Console.WriteLine($"string: {s}");
    else if (item is int n and > 0) Console.WriteLine($"positive int: {n}");
    else if (item is double d)   Console.WriteLine($"real: {d}");
    else                         Console.WriteLine($"other: {item}");
}

**Output**

text
string (5 chars): hello
positive int: 42
real: 3.14
other: True
string (5 chars): world

**Note:** `is int n and > 0` combines type and value checks so one line carries a lot of meaning.

Example 2 β€” `SwitchType`: type pattern + `switch` expression

csharp
static string Describe(Animal a) => a switch
{
    Dog d  => $"{d.Name}: woof",
    Cat c  => $"{c.Name}: meow",
    Bird b => $"{b.Name}: tweet",
    _      => "unknown animal",
};

**Output**

text
Baduk: woof
Navi: meow
Tweet: tweet
Jjong: woof

**Note:** Compare with old-style polymorphism via `virtual Sound()`. With few additions and centralized branching, pattern matching is cleaner.

Example 3 β€” `PropertyPattern`: property pattern

csharp
string zone = p switch
{
    { X: 0, Y: 0 }     => "origin",
    { X: 0 }           => "Y-axis",
    { Y: 0 }           => "X-axis",
    { X: > 0, Y: > 0 } => "Q1",
    { X: < 0, Y: < 0 } => "Q3",
    _                  => "other quadrant",
};

**Output**

text
(0, 0) β†’ origin
(0, 5) β†’ Y-axis
(7, 0) β†’ X-axis
(3, 4) β†’ Q1
(-1, -2) β†’ Q3

**Note:** `switch` expression branches are matched **top-to-bottom**, first-match wins. Put more specific conditions first.

Example 4 β€” `TuplePattern`: rock-paper-scissors

csharp
string result = (p1, p2) switch
{
    ("rock",     "scissors") => "P1 wins",
    ("paper",    "rock")     => "P1 wins",
    ("scissors", "paper")    => "P1 wins",
    var (a, b) when a == b   => "tie",
    _                        => "P2 wins",
};

**Output**

text
rock     vs scissors : P1 wins
paper    vs rock     : P1 wins
scissors vs scissors : tie
rock     vs paper    : P2 wins

**Note:** A rule table translates straight into code. `var (a, b) when ...` deconstructs and adds a condition.

Example 5 β€” `ListPattern`: list pattern

csharp
string desc = arr switch
{
    []              => "empty",
    [var only]      => $"single element ({only})",
    [1, .., 3]      => "starts 1, ends 3",
    [_, _, _]       => "exactly three",
    [var first, .. var rest] => $"first {first}, {rest.Length} remaining",
};

**Output**

text
[] β†’ empty
[42] β†’ single element (42)
[1,2,3] β†’ starts 1, ends 3
[1,99,3] β†’ starts 1, ends 3
[1,2,3,4,5] β†’ starts 1, ends 3
[9,8,7] β†’ exactly three

**Note:** Since `[1, .., 3]` comes before `[_, _, _]`, even `[1,2,3]` matches the first arm. Always be aware of order.

Full example code (src/)

src/IsPattern/IsPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/IsPattern/Program.cs

csharp
// is pattern β€” type check + variable declaration in one shot.
object[] items = ["hello", 42, 3.14, true, "world"];

foreach (object item in items)
{
    // declaration pattern: when the type matches, s is cast and bound.
    if (item is string s)
    {
        Console.WriteLine($"string ({s.Length} chars): {s}");
    }
    // Combine conditions
    else if (item is int n and > 0)
    {
        Console.WriteLine($"positive int: {n}");
    }
    else if (item is double d)
    {
        Console.WriteLine($"real: {d}");
    }
    else
    {
        Console.WriteLine($"other: {item}");
    }
}

src/ListPattern/ListPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/ListPattern/Program.cs

csharp
// List/array patterns (.NET 7+) β€” check length and position together.
int[][] arrays =
[
    [],
    [42],
    [1, 2, 3],
    [1, 99, 3],
    [1, 2, 3, 4, 5],
    [9, 8, 7],
];

foreach (int[] arr in arrays)
{
    string desc = arr switch
    {
        []              => "empty",
        [var only]      => $"single element ({only})",
        [1, .., 3]      => "starts 1, ends 3",
        [_, _, _]       => "exactly three",
        [var first, .. var rest] => $"first {first}, {rest.Length} remaining",
    };
    Console.WriteLine($"[{string.Join(",", arr)}] β†’ {desc}");
}

src/PropertyPattern/Program.cs

csharp
// Property pattern β€” match an object's property values directly.
Point[] points = [new(0, 0), new(0, 5), new(7, 0), new(3, 4), new(-1, -2)];

foreach (Point p in points)
{
    string zone = p switch
    {
        { X: 0, Y: 0 }            => "origin",
        { X: 0 }                  => "Y-axis",
        { Y: 0 }                  => "X-axis",
        { X: > 0, Y: > 0 }        => "Q1",
        { X: < 0, Y: < 0 }        => "Q3",
        _                         => "other quadrant",
    };
    Console.WriteLine($"({p.X}, {p.Y}) β†’ {zone}");
}

internal sealed record Point(int X, int Y);

src/PropertyPattern/PropertyPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/SwitchType/Program.cs

csharp
// switch expression + type pattern β€” great for branching once down an inheritance tree.
Animal[] zoo = [new Dog("Baduk"), new Cat("Navi"), new Bird("Tweet"), new Dog("Jjong")];

foreach (Animal a in zoo)
{
    string sound = Describe(a);
    Console.WriteLine(sound);
}

static string Describe(Animal a) => a switch
{
    Dog d  => $"{d.Name}: woof",
    Cat c  => $"{c.Name}: meow",
    Bird b => $"{b.Name}: tweet",
    _      => "unknown animal",   // discard pattern β€” anything else
};

internal abstract record Animal(string Name);
internal sealed record Dog(string Name) : Animal(Name);
internal sealed record Cat(string Name) : Animal(Name);
internal sealed record Bird(string Name) : Animal(Name);

src/SwitchType/SwitchType.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/TuplePattern/Program.cs

csharp
// Tuple pattern β€” match many values at once. RPS rules at a glance.
(string, string)[] games =
[
    ("rock", "scissors"),
    ("paper", "rock"),
    ("scissors", "scissors"),
    ("rock", "paper"),
];

foreach ((string p1, string p2) in games)
{
    string result = (p1, p2) switch
    {
        ("rock",     "scissors") => "P1 wins",
        ("paper",    "rock")     => "P1 wins",
        ("scissors", "paper")    => "P1 wins",
        var (a, b) when a == b   => "tie",          // when adds extra condition
        _                        => "P2 wins",
    };
    Console.WriteLine($"{p1,-8} vs {p2,-8} : {result}");
}

src/TuplePattern/TuplePattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Common Mistakes

  1. Not covering every path in a `switch` expression β€” without a `default` (`_`) you can hit `MatchFailureException`. The compiler also warns.
  2. Placing a more general pattern first so more specific patterns below it are never reachable.
  3. Forgetting a null check with property patterns β€” `obj switch { { Prop: ... } => ... }` doesn't match if obj is null. Intentional or handle in another branch.
  4. Overusing `when` clauses until your switch is essentially an `if/else if` ladder β€” at that point an `if` is more readable.
  5. Using list patterns on a huge collection β€” patterns index by length, so they suit arrays/lists, not arbitrary `IEnumerable`.

Summary

  • `is` pattern = type check + variable + extra condition
  • `switch` expression returns a value, so `var x = obj switch { ... };` reads naturally
  • Combining type/property/tuple/list patterns paints branching logic that mirrors the data shape
  • Branch order matters β€” more specific patterns first
  • `when` is a last resort; too much of it and a plain `if` is cleaner

Practice

**Practice - 21. Pattern Matching**

Problem 1 β€” Shape area calculator

  • Project folder: `Homework01/`
  • Key concepts: type pattern, `switch` expression, `record`

Requirements

  • Define these shapes as `record`s.
  • `Circle(double Radius)`
  • `Rectangle(double Width, double Height)`
  • `Triangle(double Base, double Height)`
  • Common abstract base: `abstract record Shape;`
  • Write `static double Area(Shape s)` using a `switch` expression and type patterns.
  • Print the results for this array.

```csharp Shape[] shapes = [new Circle(2), new Rectangle(3, 4), new Triangle(5, 6)]; ```

Expected output

text
Circle(2) area: 12.57
Rectangle(3, 4) area: 12.00
Triangle(5, 6) area: 15.00

Hints

  • Circle area = `Math.PI * r * r`.
  • Triangle = `0.5 * base * height`.
  • Output with `area.ToString("F2")` or `$"{area:F2}"`.

Problem 2 β€” Coordinate classifier

  • Project folder: `Homework02/`
  • Key concepts: property pattern, `when` clause

Requirements

  • Define `record Point(int X, int Y);`.
  • Write `static string Classify(Point p)` with a `switch` expression and property patterns.
  • Rules:
  • `(0, 0)` β†’ `"origin"`
  • `X == 0` and `Y != 0` β†’ `"Y-axis"`
  • `Y == 0` and `X != 0` β†’ `"X-axis"`
  • `X == Y` (and not origin) β†’ `"y=x line"`
  • Otherwise all positive β†’ `"Q1"`
  • Otherwise all negative β†’ `"Q3"`
  • Otherwise β†’ `"other quadrant"`
  • Call with this array:

```csharp Point[] points = [new(0,0), new(0,5), new(7,0), new(3,3), new(2,8), new(-1,-5), new(-3,4)]; ```

Expected output

text
(0,0)  β†’ origin
(0,5)  β†’ Y-axis
(7,0)  β†’ X-axis
(3,3)  β†’ y=x line
(2,8)  β†’ Q1
(-1,-5) β†’ Q3
(-3,4) β†’ other quadrant

Hints

  • Branch order matters β€” `(0,0)` first.
  • Use `{ X: var x, Y: var y } when x == y` to capture and compare.
  • `{ X: > 0, Y: > 0 }` covers a quadrant in one go.

Check your answer

Try it yourself, then compare against the [`answer/`](./answer/) folder.

Answer (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework01/Program.cs

csharp
Shape[] shapes = [new Circle(2), new Rectangle(3, 4), new Triangle(5, 6)];

foreach (Shape s in shapes)
{
    string label = s switch
    {
        Circle c    => $"Circle({c.Radius})",
        Rectangle r => $"Rectangle({r.Width}, {r.Height})",
        Triangle t  => $"Triangle({t.Base}, {t.Height})",
        _           => "Unknown",
    };
    Console.WriteLine($"{label} area: {Area(s):F2}");
}

// Type pattern + switch expression β€” adding a new shape requires editing only this one place.
static double Area(Shape s) => s 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 InvalidOperationException($"Unsupported: {s.GetType().Name}"),
};

internal abstract record Shape;
internal sealed record Circle(double Radius) : Shape;
internal sealed record Rectangle(double Width, double Height) : Shape;
internal sealed record Triangle(double Base, double Height) : Shape;

homework/answer/Homework02/Homework02.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework02/Program.cs

csharp
Point[] points =
[
    new(0, 0), new(0, 5), new(7, 0), new(3, 3),
    new(2, 8), new(-1, -5), new(-3, 4),
];

foreach (Point p in points)
{
    Console.WriteLine($"({p.X},{p.Y}) β†’ {Classify(p)}");
}

// Branch order matters β€” put the most specific (0,0) at the top.
static string Classify(Point p) => p switch
{
    { X: 0, Y: 0 }                          => "origin",
    { X: 0 }                                => "Y-axis",
    { Y: 0 }                                => "X-axis",
    { X: var x, Y: var y } when x == y      => "y=x line",
    { X: > 0, Y: > 0 }                      => "Q1",
    { X: < 0, Y: < 0 }                      => "Q3",
    _                                       => "other quadrant",
};

internal sealed record Point(int X, int Y);

Try It Yourself

bash
cd src/IsPattern && dotnet run
cd ../SwitchType && dotnet run
cd ../PropertyPattern && dotnet run
cd ../TuplePattern && dotnet run
cd ../ListPattern && dotnet run

Next Lecture

[22_Record_and_init](../22_Record%EC%99%80_init/) β€” The `record` type pairs perfectly with pattern matching.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—