Mastering C# Switch Expressions and Pattern Matching: A Practical Guide
Tags: csharp, cleancode, patternmatching, realworlddev
Published: 2024-07-16 15:10:45

**Introduction**

As seasoned C# developers, we've all used `switch` statements to make decisions based on values. C# 7.0 introduced pattern matching, and C# 12 took it a step further with switch expressions. This powerful combination enhances code readability, maintainability, and expressiveness, especially when dealing with complex logic and diverse input types.

**Demystifying Switch Expressions and Pattern Matching**

**Switch Expressions**

Switch expressions are a concise and expressive alternative to traditional `switch` statements. They allow you to return a value directly from a case, making them ideal for scenarios where you need a result based on the matched pattern. Here's the basic syntax:

```csharp
expression switch
{
    pattern1 => value1,
    pattern2 => value2,
    // ... more patterns
    _ => default  // Optional default case
}
```

- `expression`: The value to be matched against the patterns.
- `pattern`: A pattern that specifies the conditions for a case to match.
- `value`: The value to return if the `expression` matches the `pattern`.
- `_`: The discard pattern, used as a catch-all for unmatched cases.
- `default`: An optional default case that executes if no pattern matches.

**Pattern Matching**

Pattern matching is the core concept at play here. It allows you to compare an expression against different patterns and take corresponding actions. C# supports a rich set of patterns, including:

- **Constant Pattern:** Matches against a specific constant value.
    ```csharp
    dayOfWeek switch
    {
        DayOfWeek.Monday => "Start of the week blues",
        DayOfWeek.Friday => "TGIF!",
        // ... other weekdays
    }
    ```
- **Type Pattern:** Matches against a specific type.
    ```csharp
    object obj = "...";
    if (obj is string str)
    {
        Console.WriteLine($"String value: {str}");
    }
    else if (obj is int i)
    {
        Console.WriteLine($"Integer value: {i}");
    }
    ```
- **Relational Pattern:** Matches based on a relational operator (`<`, `>`, `<=`, `>=`, `==`, `!=`).
    ```csharp
    int age = 25;
    switch (age)
    {
        case < 18:
            Console.WriteLine("Not eligible to vote");
            break;
        case >= 18:
            Console.WriteLine("Eligible to vote");
            break;
    }
    ```
- **Property Pattern:** Matches based on the value of a property.
    ```csharp
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    Person person = new Person { Name = "Alice", Age = 30 };
    switch (person)
    {
        case Person p when p.Age >= 18:
            Console.WriteLine($"{p.Name} is an adult");
            break;
        default:
            Console.WriteLine($"{person.Name} is not an adult");
            break;
    }
    ```

- **Discard Pattern (`_`):** Captures a value but doesn't use it (useful for cases where the value isn't needed).
    ```csharp
    switch (Console.ReadLine())
    {
        case _:  // Handle any input
            Console.WriteLine("Input received");
            break;
    }
    ```
- **Logical AND (`&`), OR (`|`), and NOT (`!`):** Combine patterns for complex matching.
    ```csharp
    string input = "...";
    switch (input)
    {
        case string s when s.StartsWith("Hello") && s.EndsWith("World"):
            Console.WriteLine("Greeting received");
            break;
        case string s when s.Length > 10 | s.Contains("!"):
            Console.WriteLine("Long or emphatic input");
            break;
    }
    ```
**Real-World Use Cases of C# Switch Expressions and Pattern Matching**

In the realm of practical C# development, switch expressions and pattern matching shine in various scenarios. Let's delve into some compelling examples:

**1. Data Validation and Processing**

- **Robust Input Handling:** When dealing with user input, you can leverage switch expressions to ensure data integrity. Validate input types and extract meaningful values:

  ```csharp
  string input = Console.ReadLine();
  switch (input)
  {
      case int i:
          Console.WriteLine($"Integer value: {i}");
          // Perform integer-specific operations
          break;
      case double d:
          Console.WriteLine($"Double value: {d}");
          // Perform double-specific operations
          break;
      case string s when s.Length > 0:  // Handle non-empty strings
          Console.WriteLine($"String value: {s}");
          // Perform string-specific operations
          break;
      default:
          Console.WriteLine("Invalid input. Please enter a number or a string.");
          break;
  }
  ```

- **Complex Data Structures:** When working with complex data structures like `enum` or custom types, pattern matching allows for concise and readable validation and processing:

  ```csharp
  enum FileOperation
  {
      Create,
      Read,
      Update,
      Delete
  }

  FileOperation operation = GetFileOperationFromUser();  // Function to get user input

  switch (operation)
  {
      case FileOperation.Create:
          CreateFile();
          break;
      case FileOperation.Read:
          ReadFile();
          break;
      case FileOperation.Update: when CanUpdateFile():  // Conditional update
          UpdateFile();
          break;
      case FileOperation.Delete:
          DeleteFile();
          break;
      default:
          Console.WriteLine("Invalid operation selected.");
          break;
  }
  ```

**2. State Management and Flow Control**

- **Encapsulated State Machines:** Simplify state management logic by utilizing switch expressions for transitions based on events or conditions:

  ```csharp
  enum GameState
  {
      StartMenu,
      Playing,
      GameOver
  }

  GameState currentState = GameState.StartMenu;

  while (true)
  {
      GameState nextState = currentState;
      switch (currentState)
      {
          case GameState.StartMenu:
              if (HandleStartMenuInput())
              {
                  nextState = GameState.Playing;
              }
              break;
          case GameState.Playing:
              if (IsGameOver())
              {
                  nextState = GameState.GameOver;
              }
              // ... handle game logic
              break;
          case GameState.GameOver:
              if (HandleGameOverInput())
              {
                  nextState = GameState.StartMenu; // Or other options
              }
              break;
      }

      currentState = nextState;
  }
  ```

**3. Configuration Parsing and Deserialization**

- **Flexible Configuration Handling:** When parsing configuration files or deserializing data, switch expressions offer a clean way to handle different data formats or missing values:

  ```csharp
  IConfiguration config = LoadConfiguration();

  switch (config)
  {
      case IConfigurationSection section:
          string value = section.Value;  // Access configuration values
          break;
      case null:
          Console.WriteLine("Configuration not found.");
          break;
      default:
          Console.WriteLine("Invalid configuration format.");
          break;
  }
  ```

By effectively combining switch expressions and pattern matching, you can create more readable, maintainable, and expressive code in various C# applications.