Pattern Matching
C# pattern matching switches on shape, not just value. Type patterns, property patterns, relational, list patterns — a declarative way to deconstruct and branch.
Type, property, list, relational, switch expressions
EXAMPLE
using System;
using System.Collections.Generic;
// 1) Type pattern + 'is'
object obj = 42;
if (obj is int n) Console.WriteLine(n * 2); // n is captured if type matches
// Negated
if (obj is not string s) {
// not a string
}
// 2) Switch expression — the modern shape
string Describe(object o) => o switch
{
null => "null",
int i when i < 0 => $"negative {i}",
int i => $"int {i}",
string s => $"string '{s}'",
int[] a => $"int[{a.Length}]",
_ => $"unknown ({o.GetType().Name})",
};
Console.WriteLine(Describe(42)); // int 42
Console.WriteLine(Describe("hi")); // string 'hi'
Console.WriteLine(Describe(null)); // null
// 3) Property patterns — match by shape
record Point(int X, int Y);
record Rect(Point TopLeft, Point BottomRight);
string Quadrant(Point p) => p switch
{
{ X: > 0, Y: > 0 } => "I",
{ X: < 0, Y: > 0 } => "II",
{ X: < 0, Y: < 0 } => "III",
{ X: > 0, Y: < 0 } => "IV",
{ X: 0, Y: 0 } => "origin",
_ => "axis",
};
// 4) Recursive property pattern — drill into nested objects
int AreaOf(Rect r) => r switch
{
{ TopLeft: { X: var tx, Y: var ty }, BottomRight: { X: var bx, Y: var by } }
=> Math.Abs((bx - tx) * (by - ty)),
};
// 5) Deconstruction patterns — for records / types with Deconstruct
string Name(Point p) => p switch
{
(0, 0) => "origin",
(var x, 0) => $"on X at {x}",
(0, var y) => $"on Y at {y}",
(var x, var y) => $"({x}, {y})",
};
// 6) Discriminated-union style with sealed records
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height): Shape;
record Triangle(double Base, double Height) : Shape;
double Area(Shape s) => s switch
{
Circle { Radius: var r } => Math.PI * r * r,
Rectangle { Width: var w, Height: var h } => w * h,
Triangle { Base: var b, Height: var h } => 0.5 * b * h,
_ => throw new ArgumentException(),
};
// 7) Relational + logical patterns (C# 9+)
string TrafficLight(int speed) => speed switch
{
< 0 => "reverse",
0 => "stopped",
> 0 and <= 30 => "slow",
> 30 and <= 80 => "normal",
> 80 and < 130 => "fast",
_ => "too fast",
};
// 8) List patterns (C# 11+)
string Describe<T>(T[] arr) => arr switch
{
[] => "empty",
[var x] => $"one: {x}",
[var x, var y] => $"two: {x}, {y}",
[var first, .., var last] => $"first={first}, last={last}",
_ => $"len={arr.Length}",
};
Describe(new[] { 1, 2, 3, 4, 5 }); // first=1, last=5
// 9) Patterns inside if / when
void Handle(object e)
{
if (e is Exception ex && ex.InnerException is { Message: var inner })
Console.WriteLine($"inner: {inner}");
if (e is HttpResponseMessage { StatusCode: var sc } and not { IsSuccessStatusCode: true })
throw new InvalidOperationException($"failed {sc}");
}
// 10) Pattern matching vs visitor pattern
// Old OO: a Visitor<TResult> interface + Accept() method on every variant.
// Modern C#: sealed records + switch expression. Exhaustive (with sealed hierarchies).
// Adding a new case: switch warns about missing arm; compiler enforces.
// 11) Exhaustiveness — compiler help
// With sealed records and a switch expression, the compiler warns if you miss a case.
// Without `_`, missing variants give CS8509.
// 12) Performance
// Switch expressions on patterns compile to efficient code — usually equivalent to
// hand-written if/else chains. The compiler optimises type tests with vtable / jump tables.
Why it matters
Sealed records + switch expressions give you the same benefits as Rust enums / TypeScript discriminated unions — exhaustiveness, no boilerplate visitor pattern, and an inviting site to add behavior over time.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
object o = 42;
string r = o switch {
int n when n > 0 => "positive",
int => "non-positive int",
string => "a string",
_ => "other"
};
Try it Yourself »
Discussion
Loading…