C# Switch Expressions
Switch expressions are a modern C# feature introduced in C# 8.0 that provide a more concise and expressive way to perform conditional logic compared to traditional switch statements. They're especially useful when you need to transform data based on conditions or when pattern matching against different types.
Introduction to Switch Expressions
Traditional switch statements in C# have been around since the beginning, but they can be verbose and repetitive. Switch expressions offer a more elegant, expression-based alternative that's particularly useful when you want to:
- Assign a value based on a condition
- Transform data based on different patterns
- Return different values depending on an input
Let's explore how switch expressions work and how they differ from standard switch statements.
Basic Syntax
Here's the basic syntax of a switch expression:
result = someValue switch
{
pattern1 => expression1,
pattern2 => expression2,
_ => defaultExpression
};
This reads as: "Evaluate someValue
, and depending on which pattern it matches, return the corresponding expression."
Let's see a simple example:
string GetDayType(DayOfWeek day) => day switch
{
DayOfWeek.Saturday => "Weekend",
DayOfWeek.Sunday => "Weekend",
_ => "Weekday"
};
// Usage
Console.WriteLine(GetDayType(DateTime.Now.DayOfWeek));
If today is Tuesday, the output would be:
Weekday
If today is Saturday, the output would be:
Weekend
Switch Expressions vs Switch Statements
To understand the advantages of switch expressions, let's compare them to traditional switch statements:
Traditional Switch Statement
string GetDayType(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Saturday:
return "Weekend";
case DayOfWeek.Sunday:
return "Weekend";
default:
return "Weekday";
}
}
Modern Switch Expression
string GetDayType(DayOfWeek day) => day switch
{
DayOfWeek.Saturday => "Weekend",
DayOfWeek.Sunday => "Weekend",
_ => "Weekday"
};
The switch expression is:
- More concise (fewer lines of code)
- Expression-based (returns a value directly)
- Uses
=>
arrow syntax instead ofcase
andreturn
- Uses
_
as the discard pattern (similar todefault
)
Pattern Matching in Switch Expressions
One of the most powerful aspects of switch expressions is their ability to use pattern matching. C# supports several patterns:
Constant Patterns
Matching against constant values:
string GetTrafficLightAction(string color) => color.ToLower() switch
{
"red" => "Stop",
"yellow" => "Caution",
"green" => "Go",
_ => "Invalid color"
};
// Usage
Console.WriteLine(GetTrafficLightAction("Red")); // Output: Stop
Console.WriteLine(GetTrafficLightAction("Green")); // Output: Go
Type Patterns
Matching based on the type of an object:
string GetObjectType(object obj) => obj switch
{
int i => $"Integer: {i}",
string s => $"String: {s}",
DateTime d => $"Date: {d.ToShortDateString()}",
null => "Null value",
_ => $"Unknown type: {obj.GetType().Name}"
};
// Usage
Console.WriteLine(GetObjectType(42)); // Output: Integer: 42
Console.WriteLine(GetObjectType("Hello")); // Output: String: Hello
Console.WriteLine(GetObjectType(DateTime.Now)); // Output: Date: [current date]
Property Patterns
Matching based on object properties:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Country { get; set; }
}
string GetPersonCategory(Person person) => person switch
{
{ Age: < 18 } => "Minor",
{ Age: >= 65 } => "Senior",
{ Country: "USA" } => "American Adult",
_ => "Adult"
};
// Usage
var alice = new Person { Name = "Alice", Age = 25, Country = "USA" };
var bob = new Person { Name = "Bob", Age = 17, Country = "Canada" };
var carol = new Person { Name = "Carol", Age = 70, Country = "UK" };
Console.WriteLine(GetPersonCategory(alice)); // Output: American Adult
Console.WriteLine(GetPersonCategory(bob)); // Output: Minor
Console.WriteLine(GetPersonCategory(carol)); // Output: Senior
Tuple Patterns
Matching against multiple values simultaneously using tuples:
string GetQuadrant(int x, int y) => (x, y) switch
{
(0, 0) => "Origin",
(> 0, > 0) => "First quadrant",
(< 0, > 0) => "Second quadrant",
(< 0, < 0) => "Third quadrant",
(> 0, < 0) => "Fourth quadrant",
(_, 0) => "X-axis",
(0, _) => "Y-axis",
_ => "Impossible" // This case will never be reached
};
// Usage
Console.WriteLine(GetQuadrant(5, 3)); // Output: First quadrant
Console.WriteLine(GetQuadrant(-2, 4)); // Output: Second quadrant
Console.WriteLine(GetQuadrant(0, 0)); // Output: Origin
Console.WriteLine(GetQuadrant(0, -7)); // Output: Y-axis
Real-World Applications
Example 1: Calculating Shipping Costs
decimal CalculateShippingCost(string country, decimal orderValue) => (country, orderValue) switch
{
("USA", < 50m) => 5.99m,
("USA", _) => 0m, // Free shipping for USA orders over $50
("Canada", < 100m) => 10.99m,
("Canada", _) => 5.99m,
("Mexico", _) => 12.99m,
(_, < 100m) => 19.99m,
_ => 14.99m // International orders over $100
};
// Usage
Console.WriteLine($"Shipping to USA for $45: ${CalculateShippingCost("USA", 45m)}");
Console.WriteLine($"Shipping to USA for $65: ${CalculateShippingCost("USA", 65m)}");
Console.WriteLine($"Shipping to France for $120: ${CalculateShippingCost("France", 120m)}");
// Output:
// Shipping to USA for $45: $5.99
// Shipping to USA for $65: $0
// Shipping to France for $120: $14.99
Example 2: Exception Handling with Pattern Matching
string FormatException(Exception ex) => ex switch
{
FileNotFoundException fnf => $"File not found: {fnf.FileName}",
HttpRequestException { StatusCode: System.Net.HttpStatusCode.NotFound } => "404 - Resource not found",
HttpRequestException { StatusCode: System.Net.HttpStatusCode.Unauthorized } => "401 - Authentication required",
HttpRequestException http => $"HTTP error: {http.StatusCode}",
JsonException => "Invalid JSON format",
null => "No exception occurred",
_ => $"Unexpected error: {ex.Message}"
};
// Usage (in practice this would be used in a catch block)
try
{
// Some code that might throw exceptions
throw new FileNotFoundException("Could not find settings file", "settings.json");
}
catch (Exception ex)
{
Console.WriteLine(FormatException(ex)); // Output: File not found: settings.json
}
Example 3: State Machine
Here's a simple state machine for a vending machine using switch expressions:
enum VendingMachineState { Ready, ItemSelected, PaymentProcessing, DeliveringItem }
enum UserAction { SelectItem, InsertMoney, Cancel, TakeItem }
VendingMachineState GetNextState(VendingMachineState currentState, UserAction action) =>
(currentState, action) switch
{
(VendingMachineState.Ready, UserAction.SelectItem) => VendingMachineState.ItemSelected,
(VendingMachineState.ItemSelected, UserAction.InsertMoney) => VendingMachineState.PaymentProcessing,
(VendingMachineState.ItemSelected, UserAction.Cancel) => VendingMachineState.Ready,
(VendingMachineState.PaymentProcessing, UserAction.Cancel) => VendingMachineState.Ready,
(VendingMachineState.PaymentProcessing, _) => VendingMachineState.DeliveringItem,
(VendingMachineState.DeliveringItem, UserAction.TakeItem) => VendingMachineState.Ready,
_ => currentState // Invalid state transition, stay in current state
};
// Usage: simulate a successful purchase
var state = VendingMachineState.Ready;
Console.WriteLine($"Initial state: {state}");
state = GetNextState(state, UserAction.SelectItem);
Console.WriteLine($"After selecting item: {state}");
state = GetNextState(state, UserAction.InsertMoney);
Console.WriteLine($"After inserting money: {state}");
state = GetNextState(state, UserAction.TakeItem);
Console.WriteLine($"After taking item: {state}");
// Output:
// Initial state: Ready
// After selecting item: ItemSelected
// After inserting money: PaymentProcessing
// After taking item: DeliveringItem
Best Practices for Switch Expressions
-
Keep it readable: Just because you can put complex logic in a pattern, doesn't mean you should. Consider readability.
-
Order matters: More specific patterns should come before more general ones, as patterns are evaluated in order.
-
Use when guards for complex conditions: For more complex conditions, use
when
clauses:
var result = number switch
{
int n when n > 0 && n % 2 == 0 => "Positive even number",
int n when n > 0 => "Positive odd number",
0 => "Zero",
_ => "Negative number"
};
- Be exhaustive: Make sure your patterns cover all possible input values or include a discard (
_
) pattern.
Common Mistakes and Pitfalls
-
Forgetting the discard pattern: Always include a
_
case to handle unexpected inputs, unless you want an exception to be thrown. -
Order of patterns: Remember that patterns are evaluated in order, so more specific patterns should come first.
-
Missing comma after a pattern arm: Each pattern arm must be followed by a comma, except for the last one.
-
Using
break
orreturn
: Unlike switch statements, switch expressions don't usebreak
orreturn
keywords.
Summary
Switch expressions provide a modern, concise way to handle conditional logic in C#. They offer several advantages over traditional switch statements:
- More compact syntax
- Expression-based (returns values directly)
- Powerful pattern matching capabilities
- Support for tuple patterns for multi-value matching
- Property patterns for object property matching
These features make switch expressions an excellent choice for many conditional logic scenarios, especially when transforming data or implementing pattern-based logic.
Exercises
-
Write a switch expression that calculates a discount based on order total:
- Orders under $50: no discount
- Orders 100: 5% discount
- Orders 200: 10% discount
- Orders over $200: 15% discount
-
Create a
GetGrade
function that converts a numeric score to a letter grade using a switch expression:- 90-100: "A"
- 80-89: "B"
- 70-79: "C"
- 60-69: "D"
- Below 60: "F"
-
Write a switch expression that handles different shapes and calculates their area (hint: use type patterns).
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)