C# Switch Expressions & Pattern Matching
C# switch expressions let you choose a value by matching an input against a list of patterns. Pattern matching makes decisions based on values, types, object properties, ranges, and conditions, so it is much more expressive than comparing one variable to fixed constants. Used well, it turns nested if blocks into clear, readable control flow.
Overview: How Switch Expressions Work
A traditional switch statement runs statements. A switch expression produces a value. That difference matters: because a switch expression is an expression, you can assign its result to a variable, return it from a method, pass it to another method, or use it inside string interpolation.
Pattern matching is the matching engine behind modern switch expressions. Instead of asking only, is this equal to 3?, C# can ask richer questions: is this value an int, is it less than zero, is this object a Circle, does this record have Total greater than or equal to 100m, or does a matched variable also satisfy a when guard?
The compiler checks the arms from top to bottom. The first arm whose pattern matches is selected, its expression is evaluated, and the resulting value becomes the value of the whole switch expression. Later arms are not considered. This ordering is important when one pattern is broader than another.
Internally, the compiler lowers pattern matching into ordinary branching logic, type checks, comparisons, and property reads. There is no special runtime object called a pattern. The generated code still executes on the CLR like other C# code. For constant switches on primitive values, the compiler may generate efficient jump tables or lookup-style branches. For property and type patterns, it emits the required tests in a safe order so values are only accessed after the containing object has matched.
Syntax
object value = 42;
string result = value switch
{
int number when number > 0 => "positive integer",
string text => text,
null => "missing",
_ => "other"
};
Console.WriteLine(result);
A switch expression has these parts:
| Part | Meaning |
|---|---|
value switch |
The input expression being matched. |
pattern => expression |
An arm. If the pattern matches, the expression on the right is returned. |
when |
An optional guard. The pattern must match and the guard must be true. |
_ |
The discard pattern. It matches anything and is commonly used as the final fallback. |
| commas | Arms are separated by commas, not semicolons or colons. |
Examples
Example 1: Ranges With Relational Patterns
using System;
class Program
{
static void Main()
{
int minutesLate = 12;
string status = minutesLate switch
{
<= 0 => "On time",
<= 5 => "Small delay",
<= 30 => "Delayed",
_ => "Severely delayed"
};
Console.WriteLine(status);
}
}
Output:
Delayed
The arms are tested from top to bottom. The value 12 is not less than or equal to 0 or 5, but it is less than or equal to 30, so the switch expression returns "Delayed". Notice that each arm returns a string, so the type of status is straightforward.
Example 2: Property Patterns For Business Rules
using System;
record Order(decimal Total, bool IsPriority, string Country);
class Program
{
static decimal Shipping(Order order) => order switch
{
{ Total: >= 100m } => 0m,
{ IsPriority: true, Country: "US" } => 5m,
{ Country: "US" } => 8m,
{ Country: "CA" } => 12m,
_ => 20m
};
static void Main()
{
Order[] orders =
{
new Order(140m, false, "US"),
new Order(40m, true, "US"),
new Order(40m, false, "CA")
};
for (int i = 0; i < orders.Length; i++)
{
Console.WriteLine($"Order {i + 1}: ${Shipping(orders[i]):0.00}");
}
}
}
Output:
Order 1: $0.00
Order 2: $5.00
Order 3: $12.00
Property patterns match object shape without writing nested if statements. The first order gets free shipping because its Total is at least 100m. The priority US rule appears before the general US rule, so priority US orders receive the lower rate.
Example 3: Type And Positional Patterns
using System;
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
class Program
{
static string Describe(Shape shape) => shape switch
{
Circle(var radius) when radius > 10 => "large circle",
Circle(var radius) => $"circle area {Math.PI * radius * radius:0.0}",
Rectangle(var width, var height) when width == height => $"square {width:0.0} x {height:0.0}",
Rectangle(var width, var height) => $"rectangle area {width * height:0.0}",
_ => "unknown shape"
};
static void Main()
{
Shape[] shapes =
{
new Circle(2),
new Rectangle(4, 4),
new Rectangle(3, 5)
};
foreach (Shape shape in shapes)
{
Console.WriteLine(Describe(shape));
}
}
}
Output:
circle area 12.6
square 4.0 x 4.0
rectangle area 15.0
Records support deconstruction, so Circle(var radius) and Rectangle(var width, var height) are positional patterns. The type must match first, then the values are extracted into variables. The when guard handles the special square case after the rectangle has been matched.
How It Works Step By Step
- The input expression is evaluated once. If it is a method call or property access, that call is not repeated for every arm.
- The compiler tests each arm in source order. A broader pattern such as
_or{ Country: "US" }can hide more specific patterns that appear later. - When a pattern introduces variables, such as
Circle(var radius), those variables are available only on the right side of that arm and inside its guard. - If the pattern matches but the
whenguard is false, C# continues to the next arm. - The chosen arm expression is evaluated. All normal expression rules still apply: method calls run, exceptions can be thrown, and the result must be compatible with the switch expression type.
- If no arm matches and there is no fallback, the runtime throws
SwitchExpressionException. The compiler usually warns about this, but a warning is not the same as a complete program design.
Common Mistakes
Using Statement Syntax Inside A Switch Expression
Switch expressions use => arms separated by commas. They do not use case, colons, or break.
int score = 92;
string grade = score switch
{
>= 90: "A",
>= 80: "B",
_: "C"
};
Corrected version:
using System;
class Program
{
static void Main()
{
int score = 92;
string grade = score switch
{
>= 90 => "A",
>= 80 => "B",
_ => "C"
};
Console.WriteLine(grade);
}
}
Output:
A
Putting Broad Patterns First
The first matching arm wins. If a broad pattern comes first, a later specific pattern may never run.
string priceBand = total switch
{
>= 0m => "regular",
>= 100m => "premium",
_ => "invalid"
};
Corrected version:
using System;
class Program
{
static void Main()
{
decimal total = 125m;
string priceBand = total switch
{
>= 100m => "premium",
>= 0m => "regular",
_ => "invalid"
};
Console.WriteLine(priceBand);
}
}
Output:
premium
Best Practices
- Use switch expressions when every branch produces one value.
- Use a switch statement when each branch performs several actions, loops, or complex side effects.
- Put the most specific patterns before broader patterns.
- End with
_ =>unless you intentionally want unmatched input to throw. - Keep arm expressions short. If an arm needs many lines, call a well-named helper method.
- Use
whenguards for conditions that cannot be expressed cleanly as a pattern. - Prefer property patterns for objects with named data and positional patterns for records or tuple-like values where position is obvious.
- Do not hide expensive work in repeated property getters if the same data is needed in several arms; calculate it before the switch.
Practice Exercises
- Write a switch expression that converts an integer temperature to
"freezing","cold","warm", or"hot"using relational patterns. - Create a
Customerrecord withIsMemberandYearsActive. Use a property pattern to return a discount percentage. - Create records for
EmailNotificationandSmsNotification. Use type patterns to describe how each notification should be sent.
Summary
- A switch expression returns a value; a switch statement executes statements.
- Pattern matching can inspect constants, ranges, types, object properties, and deconstructed values.
- Arms are tested top to bottom, and the first match wins.
whenguards add extra conditions after a pattern matches.- A final discard arm,
_ =>, makes the expression exhaustive and avoids unmatched-input failures.
