C# tutorials > Modern C# Features > C# 6.0 and Later > Explain pattern matching in C# with examples of `is` and `switch` expressions.

Explain pattern matching in C# with examples of `is` and `switch` expressions.

Understanding Pattern Matching in C#

Pattern matching is a feature introduced in C# 7.0 and enhanced in later versions, allowing you to conditionally extract values from objects based on their structure and data. It provides a more concise and readable way to write code that handles different data types and object states. Two key constructs for pattern matching are the is and switch expressions.

Pattern Matching with the `is` Expression

The is expression checks if an object is of a specific type and, if so, assigns it to a new variable of that type. This allows you to directly access the properties of the object without needing a separate cast. The code becomes much cleaner and easier to read.

In this example, if shape is a Circle, it's automatically cast to a Circle object named c, and you can access c.Radius. The same logic applies to the Rectangle case.

public static string GetShapeDescription(object shape)
{
    if (shape is Circle c)
    {
        return $"This is a circle with radius {c.Radius}";
    }
    else if (shape is Rectangle r)
    {
        return $"This is a rectangle with width {r.Width} and height {r.Height}";
    }
    else
    {
        return "Unknown shape.";
    }
}

Pattern Matching with the `switch` Expression

The switch expression provides a more powerful way to perform pattern matching, allowing you to handle multiple cases in a concise manner. Each case can include a type pattern and a variable declaration. It can even match against null values.

The switch expression evaluates the shape object and executes the code block corresponding to the matching pattern. Similar to the is example, it casts the object to the specified type and assigns it to a variable for easy access to its properties.

public static string GetShapeDescriptionSwitch(object shape)
{
    switch (shape)
    {
        case Circle c:
            return $"This is a circle with radius {c.Radius}";
        case Rectangle r:
            return $"This is a rectangle with width {r.Width} and height {r.Height}";
        case null:
            return "Shape is null.";
        default:
            return "Unknown shape.";
    }
}

Concepts Behind the Snippet

  • Type Pattern: Checks if an object is of a particular type.
  • Variable Declaration Pattern: Declares a new variable of the specified type and assigns the object to it if the type matches.
  • Constant Pattern: Matches against a specific constant value, such as null.
  • Positional Pattern: (Available in later C# versions) Matches based on the deconstruction of an object into its constituent parts.
  • Property Pattern: (Available in later C# versions) Matches based on the values of specific properties of an object.

Real-Life Use Case

Consider a scenario where you're processing different types of commands. Using pattern matching, you can easily determine the type of command and execute the appropriate action.

In this example, the ProcessCommand method takes an ICommand object and uses a switch expression to determine if it's a CreateUserCommand or an UpdateUserCommand. It then executes the command accordingly. This removes the need for multiple if statements or type checks, making the code more maintainable.

public interface ICommand { void Execute(); }
public class CreateUserCommand : ICommand { public string Username { get; set; } public void Execute() { Console.WriteLine($"Creating user {Username}"); } }
public class UpdateUserCommand : ICommand { public int UserId { get; set; } public string NewUsername { get; set; } public void Execute() { Console.WriteLine($"Updating user {UserId} to {NewUsername}"); } }

public static void ProcessCommand(ICommand command)
{
    switch (command)
    {
        case CreateUserCommand createUser:
            createUser.Execute();
            break;
        case UpdateUserCommand updateUser:
            updateUser.Execute();
            break;
        default:
            Console.WriteLine("Unknown command.");
            break;
    }
}

Best Practices

  • Prioritize Readability: Use pattern matching to make your code more readable and understandable.
  • Handle All Cases: Ensure you have a default case in your switch expressions to handle unexpected or unknown types.
  • Avoid Redundant Checks: Don't use pattern matching if a simple type check is sufficient. It's most beneficial when you need to access properties of the matched object.
  • Use Discards (_): If you only care about the type and not the variable, use a discard (_) to avoid compiler warnings about unused variables (e.g., case Circle _:).

Interview Tip

Be prepared to explain the benefits of pattern matching over traditional type checking and casting. Highlight the improved readability, conciseness, and safety it provides. Also, be ready to discuss the different types of patterns available and when to use each one.

When to Use Them

  • Discriminated Unions: When dealing with types that can be one of several different options (like different shapes or command types).
  • Object Validation: When you need to check the properties of an object against specific criteria.
  • Refactoring Legacy Code: When you want to simplify complex conditional logic in existing code.

Memory Footprint

Pattern matching itself doesn't add significant overhead. The memory footprint is primarily determined by the objects being matched and the variables being created. If the matched object already exists, there's no additional memory allocation for casting with is or switch as the cast is essentially a compile-time check. Just remember to avoid allocating large temporary variables inside of your pattern matching blocks.

Alternatives

  • Traditional Type Checking and Casting: Using if (obj is Type) followed by a cast to access the object's properties. This approach is less concise and can lead to redundant code.
  • Virtual Methods: If you're dealing with a class hierarchy, you could use virtual methods to handle different types. However, this approach requires modifying the class hierarchy and may not be suitable in all cases.

Pros

  • Improved Readability: Makes code easier to understand and maintain.
  • Conciseness: Reduces the amount of code required to perform type checking and casting.
  • Type Safety: Avoids the need for explicit casts, reducing the risk of runtime errors.
  • Expressiveness: Allows you to express complex logic in a clear and concise manner.

Cons

  • Learning Curve: Requires understanding the different types of patterns and how to use them effectively.
  • Potential Overuse: Can be tempting to use pattern matching even when simpler solutions are available.

FAQ

  • What is the difference between `is` and `as`?

    The is operator checks if an object is of a specific type and returns a boolean value (true or false). The as operator attempts to cast an object to a specific type and returns the object if the cast is successful; otherwise, it returns null. The is operator in conjunction with pattern matching can declare a new variable of the target type directly if the type check passes. This avoids the need for a separate cast.

  • Can I use pattern matching with properties?

    Yes, you can use property patterns (introduced in C# 8.0 and later) to match objects based on the values of their properties. For example: case Person { Age: > 18, Name: "John" }:. This allows you to check multiple conditions on an object's properties in a single pattern.

  • Does pattern matching introduce performance overhead?

    In most cases, the performance overhead of pattern matching is minimal. The compiler optimizes pattern matching expressions to be as efficient as possible. However, very complex patterns with many conditions might have a slight performance impact, but it's unlikely to be significant in most real-world scenarios. Always profile your code if performance is a concern.