Switch Expressions: Concise Branching in C#
The traditional switch statement in C# has always been verbose. Each case needs a case keyword, a colon, a body, and a break. Switch expressions, introduced in C# 8, strip all of that away and produce a value directly. Combined with pattern matching, they become one of the most expressive features in the language.
From Statement to Expression
A switch expression replaces a multi-line statement with a compact expression that returns a value:
// Switch statement (traditional)
string description;
switch (statusCode)
{
case 200:
description = "OK";
break;
case 404:
description = "Not Found";
break;
case 500:
description = "Internal Server Error";
break;
default:
description = "Unknown";
break;
}
// Switch expression
var description = statusCode switch
{
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Unknown"
};
The _ discard pattern serves as the default arm. Each arm uses => to separate the pattern from the result. No break statements, no intermediate variables.
Pattern Matching in Arms
Switch expressions support every pattern type available in C#. This is where they truly outshine traditional switch statements:
public static string Describe(object value) => value switch
{
null => "nothing",
int n when n < 0 => "a negative number",
int n => $"the number {n}",
string { Length: 0 } => "an empty string",
string s => $"the string \"{s}\"",
IEnumerable<int> items => $"a collection of {items.Count()} integers",
_ => $"a {value.GetType().Name}"
};
Type patterns, property patterns, relational patterns, and guard clauses (when) all work naturally within switch expressions.
Tuple Patterns
Switch expressions handle multiple inputs elegantly through tuple patterns:
public static string RockPaperScissors(string player1, string player2)
=> (player1, player2) switch
{
("rock", "scissors") => "Player 1 wins",
("scissors", "paper") => "Player 1 wins",
("paper", "rock") => "Player 1 wins",
(var a, var b) when a == b => "Draw",
_ => "Player 2 wins"
};
This pattern is particularly useful for state machines and decision tables where the outcome depends on a combination of values.
Exhaustiveness Checking
The compiler verifies that switch expressions cover all possible inputs. For enum types, it warns about unhandled values:
public enum Season { Spring, Summer, Autumn, Winter }
public static string GetActivity(Season season) => season switch
{
Season.Spring => "Gardening",
Season.Summer => "Swimming",
Season.Autumn => "Hiking",
// CS8509: The switch expression does not handle all possible values
// Missing: Season.Winter
};
This compile-time safety is one of the strongest arguments for switch expressions over if-else chains. The compiler becomes your safety net when you add new enum values.
Nested Switch Expressions
Switch expressions can be nested, though readability should guide how deep you go:
public static decimal CalculateShipping(Order order) => order.Priority switch
{
Priority.Express => order.Weight switch
{
<= 1.0m => 9.99m,
<= 5.0m => 14.99m,
_ => 24.99m
},
Priority.Standard => order.Weight switch
{
<= 1.0m => 4.99m,
<= 5.0m => 7.99m,
_ => 12.99m
},
_ => 0m // Free shipping for other tiers
};
For deeply nested logic, consider extracting inner expressions into separate methods to maintain clarity.
Common Patterns in Practice
Mapping enums to values:
public static ConsoleColor ToConsoleColour(LogLevel level) => level switch
{
LogLevel.Error => ConsoleColor.Red,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Info => ConsoleColor.White,
LogLevel.Debug => ConsoleColor.Gray,
_ => ConsoleColor.White
};
Transforming ranges:
public static string GradeStudent(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
Handling discriminated unions (with records):
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
_ => throw new ArgumentException($"Unknown shape: {shape.GetType().Name}")
};
When to Use Switch Expressions
Switch expressions are ideal when you need to map an input to an output based on its shape, type, or value. They work best when each arm is a single expression. If an arm requires multiple statements, you either need a traditional switch statement or should extract the logic into a method.
The rule of thumb: if you are assigning a single variable based on a condition, a switch expression is almost always clearer than the alternatives.