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.
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
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`.
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).
string label = day switch
{
1 => "Mon", 2 => "Tue", 3 => "Wed",
_ => "other",
};3) Type pattern β inheritance/interface branching
string sound = animal switch
{
Dog d => $"{d.Name} woof",
Cat c => $"{c.Name} meow",
_ => "quiet...",
};4) Property pattern β check object properties
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
(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
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
string s = age switch
{
int a when a < 0 => "invalid",
int a when a < 20 => "teen",
_ => "adult",
};Examples
Example 1 β `IsPattern`: `is` pattern
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**
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
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**
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
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**
(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
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**
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
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**
[] β 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
<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
// 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
<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
// 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
// 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
<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
// 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
<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
// 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
<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
- Not covering every path in a `switch` expression β without a `default` (`_`) you can hit `MatchFailureException`. The compiler also warns.
- Placing a more general pattern first so more specific patterns below it are never reachable.
- Forgetting a null check with property patterns β `obj switch { { Prop: ... } => ... }` doesn't match if obj is null. Intentional or handle in another branch.
- Overusing `when` clauses until your switch is essentially an `if/else if` ladder β at that point an `if` is more readable.
- 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
Circle(2) area: 12.57
Rectangle(3, 4) area: 12.00
Triangle(5, 6) area: 15.00Hints
- 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
(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 quadrantHints
- 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
<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
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
<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
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
cd src/IsPattern && dotnet run
cd ../SwitchType && dotnet run
cd ../PropertyPattern && dotnet run
cd ../TuplePattern && dotnet run
cd ../ListPattern && dotnet runNext Lecture
[22_Record_and_init](../22_Record%EC%99%80_init/) β The `record` type pairs perfectly with pattern matching.
All lecture materials and example code are openly available on GitHub.
View on GitHub β