A switch expression maps an input value to a result. It was added in C# 8 and replaces the common pattern of a switch statement whose every case just returns or assigns something:
// C# 8 and later
string StatusText(int code) => code switch
{
200 => "OK",
301 or 302 => "Redirect", // 'or' needs C# 9
404 => "Not Found",
>= 500 => "Server Error", // relational patterns need C# 9
_ => "Unknown"
};
Read it top to bottom: the value before switch is tested against each arm in order, and the first arm whose pattern matches supplies the result. There is no case, no break and no default keyword; _ (the discard pattern) matches anything and serves as the fallback.
In C# 7, the same mapping is a switch statement in a small method, with return in each case:
Output:
200 OK
302 Redirect
404 Not Found
503 Server Error
418 Unknown
The expression form is shorter mainly because each arm is one line and the method body is gone. It is also an expression, so it can sit anywhere a value can: an argument, an interpolation hole, a return, a field initializer.
Syntax Rules
var result = input switch
{
pattern1 => value1,
pattern2 when condition => value2,
_ => fallback
};
- The input comes first, then
switch. This is the reverse of the statement form. - Each arm is
pattern => expression. The right side must be a single expression; for more logic, call a method. - Arms are separated by commas. A trailing comma after the last arm is allowed.
- The whole thing is an expression, so a statement that uses it ends with
;after the closing brace. - All arms must produce values of a common type.
var x = n switch { 0 => "zero", _ => 0 };does not compile (CS8506, no best type). Since C# 9 a declared target type settles it:object x = n switch { 0 => "zero", _ => 0 };compiles. - Every arm must produce a value, so an arm cannot call a
voidmethod such asConsole.WriteLine, and a switch expression cannot stand alone as a statement (CS0201). For actions, use the switch statement. - An arm can throw instead of producing a value:
_ => throw new ArgumentOutOfRangeException(nameof(input)). - The compiler rejects an arm that can never be reached because an earlier arm already covers it, with error CS8510,
The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.Put specific arms before general ones.
Relational and Logical Patterns (C# 9)
C# 9 added <, <=, >, >= as patterns, and the combinators and, or and not. They make range tables read like a specification:
// C# 9 and later
string Grade(int score) => score switch
{
< 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
string AgeGroup(int age) => age switch
{
< 13 => "child",
>= 13 and <= 19 => "teen",
_ => "adult"
};
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Because arms are tried in order, >= 80 only sees scores below 90. Relational patterns only take constants on the right; to compare against a variable, use a when guard. The C# 7 equivalent of a range table is an if chain:
Output:
95 -> A
83 -> B
71 -> C
64 -> D
12 -> F
not is most often used as not null: x is not null, or an arm not null => x.Name.
Property Patterns
A property pattern matches on the members of an object: { Property: pattern, ... }. Combined with a switch expression, it expresses business rules without a chain of && conditions:
// C# 8 and later
decimal ShippingCost(Order order) => order switch
{
{ Total: >= 100m } => 0m, // relational inside: C# 9
{ Country: "US", Express: true } => 15m,
{ Country: "US" } => 5m,
{ Express: true } => 30m,
null => throw new ArgumentNullException(nameof(order)),
_ => 12m
};
Every listed property must match for the arm to match. An empty property pattern { } matches any non-null value. C# 10 added extended property patterns for nested members: { Customer.Address.Country: "US" } instead of { Customer: { Address: { Country: "US" } } }.
Tuple Patterns
Switching on a tuple matches several values at once, which is the clean way to express a decision table:
// C# 9 and later (the 'or' pattern)
string Winner(string a, string b) => (a, b) switch
{
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "player 1",
var (x, y) when x == y => "draw",
_ => "player 2"
};
var (x, y) deconstructs the tuple into two variables that the when guard can compare. In C# 7 the usual substitute is to combine the values into one key, or to use a dictionary of outcomes:
Output:
player 1
player 2
draw
Type Patterns and when
An arm can test the runtime type and bind a variable, and a when clause adds any condition the pattern syntax cannot express:
// C# 8 and later
double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r when r.Width == r.Height => r.Width * r.Width,
Rectangle r => r.Width * r.Height,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new NotSupportedException(shape.GetType().Name)
};
The C# 7 version uses the is type pattern in an if chain, which binds the variable the same way:
Output:
Circle: 12.57
Rectangle: 13.50
Exhaustiveness and SwitchExpressionException
A switch expression must produce a value for every input. When the compiler can see inputs that no arm matches, it warns:
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.
It is a warning, not an error, so the code still builds. If an unmatched value arrives at run time, the expression throws System.Runtime.CompilerServices.SwitchExpressionException, whose message ends with the unmatched value (Unmatched value was 3.).
Enums get a special case. Even when every named member has an arm, an enum variable can hold any integer ((Size)3), so the compiler reports CS8524 for "an unnamed enum value":
// C# 8 and later
enum Size { Small, Medium, Large }
string Code(Size s) => s switch
{
Size.Small => "S",
Size.Medium => "M",
Size.Large => "L",
// warning CS8524 without the next arm
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
Add an explicit _ arm that throws with a useful exception. That documents the assumption, silences the warning, and gives a clearer error than SwitchExpressionException if a bad value ever shows up.
A Dictionary as the Alternative
When the arms are pure constant-to-value pairs and the table might grow or come from configuration, a Dictionary does the same job in any C# version and can be built at run time:
Output:
19.00
11.50
0
An unknown country gets a rate of 0m. A decimal product has as many decimal places as its two operands together, which is why 100m * 0.19m prints 19.00 and the zero rate prints 0.
Switch Expression Versus Switch Statement
| switch statement | switch expression | |
|---|---|---|
| Available since | C# 1 | C# 8 |
| Produces a value | no | yes |
| Syntax per branch | case X: ... break; | X => value, |
| Fallback | default: | _ => |
| Several statements per branch | yes | no, call a method |
| Exhaustiveness warning | no | yes (CS8509) |
| No match at run time | nothing runs | SwitchExpressionException |
Both forms take the same patterns (types, constants, properties, tuples, relational), so learning one teaches the other. The choice is about shape: a value goes through the expression, a sequence of actions goes through the statement.
Frequently Asked Questions
What is a switch expression in C#?
A switch expression, added in C# 8, evaluates to a value: string text = code switch { 200 => "OK", 404 => "Not Found", _ => "Unknown" };. The input goes before the switch keyword, each arm is pattern => result, arms are separated by commas, and there is no case, break or default keyword.
What is the default case in a C# switch expression?
The discard pattern _ matches anything, so _ => "Unknown" as the last arm plays the role of default. Without a catch-all, the compiler warns (CS8509) when some input is not covered, and an unmatched value at run time throws SwitchExpressionException.
How do I match multiple values in one arm of a switch expression?
Use the or pattern from C# 9: "sat" or "sun" => "weekend". For ranges, combine relational patterns with and: >= 13 and <= 19 => "teen". In C# 8 you would list the values as separate arms with the same result.
Can I use when in a C# switch expression?
Yes. An arm can add a guard after its pattern: Order o when o.Total > 100 => 0m. The arm matches only if the pattern matches and the when condition is true; otherwise evaluation continues with the next arm.
Should I use a switch expression or a switch statement?
Use the expression when every branch only produces a value, such as mapping a status to a label or computing a price. Use the statement when branches run several statements, have side effects or need to leave a loop. The expression also gives you exhaustiveness warnings the statement does not.