Mastering C# Switch Expressions and Pattern Matching: A Practical Guide

waelhabbal

waelhabbal

Posted on July 16, 2024

Mastering C# Switch Expressions and Pattern Matching: A Practical Guide

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:

expression switch
{
    pattern1 => value1,
    pattern2 => value2,
    // ... more patterns
    _ => default  // Optional default case
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

    dayOfWeek switch
    {
        DayOfWeek.Monday => "Start of the week blues",
        DayOfWeek.Friday => "TGIF!",
        // ... other weekdays
    }
    
  • Type Pattern: Matches against a specific type.

    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 (<, >, <=, >=, ==, !=).

    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.

    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).

    switch (Console.ReadLine())
    {
        case _:  // Handle any input
            Console.WriteLine("Input received");
            break;
    }
    
  • Logical AND (&), OR (|), and NOT (!): Combine patterns for complex matching.

    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:
  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;
  }
Enter fullscreen mode Exit fullscreen mode
  • Complex Data Structures: When working with complex data structures like enum or custom types, pattern matching allows for concise and readable validation and processing:
  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;
  }
Enter fullscreen mode Exit fullscreen mode

2. State Management and Flow Control

  • Encapsulated State Machines: Simplify state management logic by utilizing switch expressions for transitions based on events or conditions:
  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;
  }
Enter fullscreen mode Exit fullscreen mode

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:
  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;
  }
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
waelhabbal
waelhabbal

Posted on July 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related