You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Switch Expressions

Use expression-based switches with concise syntax and exhaustiveness checking

Switch Expressions in C#

Modern C# revolutionizes pattern matching with switch expressions, offering a concise syntax that reduces boilerplate and improves code readability. Master switch expressions with free flashcards and spaced repetition to solidify your understanding of this powerful language feature. This lesson covers switch expression syntax, pattern matching techniques, when clauses, and practical applications—essential concepts for writing clean, maintainable C# code.

Welcome to Switch Expressions! 💻

Traditional switch statements in C# have served us well for decades, but they often require verbose syntax with explicit case labels, break statements, and careful consideration of fall-through behavior. Switch expressions, introduced in C# 8.0 and enhanced in subsequent versions, transform the switch construct from a statement into an expression that returns a value directly.

Think of switch expressions as the evolution from imperative instructions ("do this, then do that") to declarative specifications ("given this input, the result is that"). Instead of executing multiple statements and maintaining state across cases, switch expressions evaluate to a single value based on pattern matching—making your code more functional and easier to reason about.

Core Concepts: Understanding Switch Expressions 🎯

The Fundamental Syntax Shift

The most striking difference between switch statements and switch expressions is their fundamental purpose. A switch statement executes code blocks, while a switch expression evaluates to a value.

Traditional Switch Statement:

string result;
switch (dayOfWeek)
{
    case DayOfWeek.Monday:
        result = "Start of work week";
        break;
    case DayOfWeek.Friday:
        result = "TGIF!";
        break;
    case DayOfWeek.Saturday:
    case DayOfWeek.Sunday:
        result = "Weekend!";
        break;
    default:
        result = "Midweek";
        break;
}

Modern Switch Expression:

string result = dayOfWeek switch
{
    DayOfWeek.Monday => "Start of work week",
    DayOfWeek.Friday => "TGIF!",
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend!",
    _ => "Midweek"
};

Notice the dramatic reduction in ceremony: no case keywords, no break statements, no repeated variable assignments. The switch expression reads like a mapping from inputs to outputs.

Key Structural Elements

ElementSyntaxPurpose
Switch targetexpression switchValue being matched against patterns
Armspattern => resultEach pattern-result pair
Discard pattern_ => default_valueCatch-all for unmatched cases
Pattern combinatorsor, and, notCombine multiple patterns

Pattern Types in Switch Expressions

Switch expressions support multiple pattern types, making them incredibly versatile:

1. Constant Patterns - Match specific constant values:

int score = 85;
string grade = score switch
{
    100 => "Perfect!",
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

2. Type Patterns - Match and cast types:

object obj = "Hello";
string description = obj switch
{
    int i => $"Integer: {i}",
    string s => $"String of length {s.Length}",
    bool b => $"Boolean: {b}",
    null => "Null value",
    _ => "Unknown type"
};

3. Property Patterns - Match based on object properties:

public record Person(string Name, int Age);

string category = person switch
{
    { Age: < 13 } => "Child",
    { Age: < 20 } => "Teenager",
    { Age: < 65 } => "Adult",
    { Age: >= 65 } => "Senior",
    _ => "Unknown"
};

4. Positional Patterns - Deconstruct tuples or deconstructible types:

(int x, int y) point = (3, 4);
string quadrant = point switch
{
    (0, 0) => "Origin",
    (> 0, > 0) => "Quadrant I",
    (< 0, > 0) => "Quadrant II",
    (< 0, < 0) => "Quadrant III",
    (> 0, < 0) => "Quadrant IV",
    _ => "On an axis"
};

When Clauses: Adding Conditional Guards 🛡️

Sometimes a pattern alone isn't sufficient—you need additional conditions. When clauses let you add boolean expressions to patterns:

double CalculateDiscount(Customer customer, decimal orderTotal) =>
    customer switch
    {
        { IsPremium: true } when orderTotal > 1000 => 0.20,
        { IsPremium: true } when orderTotal > 500 => 0.15,
        { IsPremium: true } => 0.10,
        { YearsActive: > 5 } when orderTotal > 500 => 0.12,
        { YearsActive: > 5 } => 0.08,
        _ => 0.05
    };

The when clause is evaluated only after the pattern matches successfully. This allows you to refine matches with complex business logic.

💡 Tip: When clauses are evaluated in order from top to bottom. Place more specific patterns before more general ones to ensure correct matching.

Exhaustiveness and the Discard Pattern

Switch expressions must be exhaustive—every possible input must have a matching arm. The compiler will warn you if your switch expression isn't exhaustive:

// ⚠️ Warning: Switch expression doesn't handle all possible values
enum TrafficLight { Red, Yellow, Green }

string action = light switch
{
    TrafficLight.Red => "Stop",
    TrafficLight.Green => "Go"
    // Missing Yellow case!
};

The discard pattern (_) serves as a catch-all that ensures exhaustiveness:

string action = light switch
{
    TrafficLight.Red => "Stop",
    TrafficLight.Green => "Go",
    _ => "Caution"  // Handles Yellow and any future values
};

🧠 Mnemonic: Think of _ as the "whatever" pattern—"whatever input you give me that hasn't matched yet, here's what to return."

Practical Examples with Detailed Explanations 🔧

Example 1: HTTP Status Code Categorization

Let's categorize HTTP status codes using switch expressions with relational patterns:

public static string CategorizeHttpStatus(int statusCode) =>
    statusCode switch
    {
        >= 200 and < 300 => "Success",
        >= 300 and < 400 => "Redirection",
        >= 400 and < 500 => "Client Error",
        >= 500 and < 600 => "Server Error",
        100 or 101 or 102 => "Informational",
        _ => "Unknown Status Code"
    };

// Usage
Console.WriteLine(CategorizeHttpStatus(404));  // "Client Error"
Console.WriteLine(CategorizeHttpStatus(200));  // "Success"
Console.WriteLine(CategorizeHttpStatus(503));  // "Server Error"

Why this works: The and combinator lets us express range conditions elegantly. The relational patterns (>=, <) are evaluated in order, so we can structure our logic from specific to general. The or combinator handles the less common informational codes concisely.

Example 2: Shape Area Calculator with Type and Property Patterns

Here's a practical example combining type patterns, property patterns, and positional patterns:

public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;

public static double CalculateArea(Shape shape) =>
    shape 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,
        null => throw new ArgumentNullException(nameof(shape)),
        _ => throw new NotSupportedException($"Unknown shape: {shape.GetType().Name}")
    };

// Usage
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);

Console.WriteLine(CalculateArea(circle));     // 78.54
Console.WriteLine(CalculateArea(rectangle));  // 24

Key insights:

  • Type patterns (Circle, Rectangle, Triangle) automatically cast the input to the matched type
  • Property patterns with var declarations capture property values for use in the result expression
  • Explicit null handling prevents null reference exceptions
  • The discard pattern catches unexpected types, throwing a meaningful exception

Example 3: Command Processing with When Clauses

This example demonstrates how when clauses enable complex conditional logic:

public record Command(string Name, string[] Args, bool IsElevated);

public static string ProcessCommand(Command cmd) =>
    cmd switch
    {
        { Name: "delete" } when !cmd.IsElevated 
            => "Error: Delete requires elevated privileges",
        
        { Name: "delete", Args.Length: 0 } 
            => "Error: Delete requires file path argument",
        
        { Name: "delete", Args: var args } when args.Length > 0 
            => $"Deleting {args.Length} file(s)...",
        
        { Name: "list", Args.Length: 0 } 
            => "Listing current directory...",
        
        { Name: "list", Args: [var path] } 
            => $"Listing contents of {path}...",
        
        { Name: var name } when name.Length > 20 
            => "Error: Command name too long",
        
        _ => "Unknown command"
    };

// Usage
var cmd1 = new Command("delete", new[] { "file.txt" }, false);
var cmd2 = new Command("delete", new[] { "file.txt" }, true);
var cmd3 = new Command("list", Array.Empty<string>(), false);

Console.WriteLine(ProcessCommand(cmd1));  // "Error: Delete requires elevated privileges"
Console.WriteLine(ProcessCommand(cmd2));  // "Deleting 1 file(s)..."
Console.WriteLine(ProcessCommand(cmd3));  // "Listing current directory..."

Advanced techniques shown:

  • When clauses checking multiple conditions
  • Property patterns accessing array properties (Args.Length)
  • List patterns ([var path]) for single-element arrays
  • Layered validation (privilege check before argument check)

Example 4: Tuple Pattern Matching for Game Logic

Tuple patterns are excellent for matching combinations of values:

public enum PlayerAction { Attack, Defend, Heal, Special }
public enum EnemyState { Aggressive, Defensive, Stunned, Fleeing }

public static string ResolveTurn(PlayerAction player, EnemyState enemy) =>
    (player, enemy) switch
    {
        (PlayerAction.Attack, EnemyState.Defensive) 
            => "Your attack is blocked! -10 damage",
        
        (PlayerAction.Attack, EnemyState.Stunned) 
            => "Critical hit on stunned enemy! -50 damage",
        
        (PlayerAction.Attack, _) 
            => "Normal attack! -25 damage",
        
        (PlayerAction.Defend, EnemyState.Aggressive) 
            => "Enemy's fierce attack absorbed by defense!",
        
        (PlayerAction.Heal, EnemyState.Aggressive) 
            => "Risky heal! Enemy attacks while you recover. +20 HP, -15 HP",
        
        (PlayerAction.Special, EnemyState.Fleeing) 
            => "Special attack misses fleeing enemy!",
        
        (PlayerAction.Special, _) 
            => "Special ability unleashed! -40 damage + stun",
        
        (_, EnemyState.Fleeing) 
            => "Enemy is fleeing! Battle ending...",
        
        _ => "Turn resolved normally"
    };

Pattern matching power: Tuple patterns let you match combinations of values declaratively. Notice how we can mix specific values with discard patterns to handle broad categories (e.g., (PlayerAction.Attack, _) matches attack against any enemy state).

Common Mistakes to Avoid ⚠️

Mistake 1: Forgetting the Switch Expression Returns a Value

// ❌ WRONG: Trying to use switch expression as a statement
dayOfWeek switch
{
    DayOfWeek.Monday => Console.WriteLine("Monday"),
    DayOfWeek.Friday => Console.WriteLine("Friday"),
    _ => Console.WriteLine("Other day")
};

Switch expressions must evaluate to a value. If you need side effects like Console.WriteLine, use a traditional switch statement or assign the result:

// ✅ CORRECT: Assign the result
string message = dayOfWeek switch
{
    DayOfWeek.Monday => "Monday",
    DayOfWeek.Friday => "Friday",
    _ => "Other day"
};
Console.WriteLine(message);

Mistake 2: Incorrect Pattern Order

// ❌ WRONG: General pattern before specific ones
int number = 100;
string result = number switch
{
    _ => "Any number",  // This matches everything!
    100 => "One hundred",  // Never reached
    > 50 => "Greater than 50"  // Never reached
};

Patterns are evaluated top-to-bottom, and the first match wins:

// ✅ CORRECT: Specific patterns first
string result = number switch
{
    100 => "One hundred",
    > 50 => "Greater than 50",
    _ => "Fifty or less"
};

Mistake 3: Non-Exhaustive Switch Without Discard

// ❌ WRONG: Missing cases throws exception at runtime
enum Color { Red, Green, Blue, Yellow }

string GetColorCode(Color color) => color switch
{
    Color.Red => "#FF0000",
    Color.Green => "#00FF00",
    Color.Blue => "#0000FF"
    // Missing Yellow! Throws SwitchExpressionException at runtime
};

Always ensure exhaustiveness:

// ✅ CORRECT: Handle all cases
string GetColorCode(Color color) => color switch
{
    Color.Red => "#FF0000",
    Color.Green => "#00FF00",
    Color.Blue => "#0000FF",
    Color.Yellow => "#FFFF00",
    _ => "#000000"  // Safety net for future enum values
};

Mistake 4: Confusing Pattern Syntax

// ❌ WRONG: Using == or && instead of pattern syntax
person switch
{
    { Age == 18 } => "Just adult",  // Syntax error!
    { Age > 18 && Age < 65 } => "Adult"  // Syntax error!
};

Use relational patterns and and combinator:

// ✅ CORRECT: Proper pattern syntax
person switch
{
    { Age: 18 } => "Just adult",
    { Age: > 18 and < 65 } => "Adult",
    _ => "Other"
};

Mistake 5: Overly Complex When Clauses

// ❌ POOR PRACTICE: When clause doing too much
value switch
{
    var x when x > 0 && x < 10 && x % 2 == 0 && IsValid(x) && HasPermission(x) 
        => "Complex condition met"
};

Extract complex logic into helper methods:

// ✅ BETTER: Readable and testable
bool IsEligible(int x) => x > 0 && x < 10 && x % 2 == 0 && IsValid(x) && HasPermission(x);

value switch
{
    var x when IsEligible(x) => "Complex condition met",
    _ => "Condition not met"
};

Key Takeaways 🎓

Switch expressions are expressions, not statements - they must evaluate to a value and can be used anywhere an expression is valid (assignments, return statements, method arguments)

Exhaustiveness is mandatory - every possible input must match at least one pattern arm, or you'll get a runtime exception. Use the discard pattern _ as a safety net

Pattern order matters - arms are evaluated top-to-bottom, and the first match wins. Always place more specific patterns before more general ones

Multiple pattern types available - constant patterns, type patterns, property patterns, positional patterns, and list patterns can all be combined for powerful matching

When clauses add flexibility - add conditional guards to patterns when simple pattern matching isn't enough, but keep them simple and readable

Combinators enhance expressiveness - use and, or, and not to combine patterns declaratively instead of complex boolean logic

Cleaner than switch statements - no break statements, no fall-through concerns, no repeated assignments—just pure mapping from inputs to outputs

📋 Quick Reference: Switch Expression Syntax

FeatureSyntaxExample
Basic structurevalue switch { arms }x switch { 1 => "one" }
Constant patternconstant => result42 => "answer"
Relational pattern> value, < value, >= value>= 100 => "high"
Type patternType var => resultstring s => s.Length
Property pattern{ Prop: pattern }{ Age: > 18 }
Positional pattern(pattern, pattern)(> 0, > 0) => "Q1"
List pattern[pattern, pattern][1, 2, 3] => "exact"
Discard pattern_ => result_ => "default"
When clausepattern when conditionvar x when x > 0
Or combinatorpattern1 or pattern21 or 2 or 3
And combinatorpattern1 and pattern2> 0 and < 100
Not combinatornot patternnot null

🤔 Did You Know?

Switch expressions are part of C#'s journey toward functional programming paradigms. The syntax was inspired by pattern matching in F#, Scala, and Haskell. Before C# 8.0, developers often resorted to dictionaries or long if-else chains to achieve similar conciseness. Switch expressions reduce cognitive load by expressing intent more directly—research shows that developers can parse a well-written switch expression 30-40% faster than equivalent if-else chains!

📚 Further Study

Ready to deepen your pattern matching expertise? Check out these resources:

  1. Microsoft Documentation - Pattern Matching: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
  2. C# 8.0 Switch Expression Deep Dive: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression
  3. Advanced Pattern Matching Techniques: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching-tutorial

Master these concepts and you'll write more expressive, maintainable C# code! 🚀