C# > Functional Programming > Pattern Matching > Positional Patterns

Positional Pattern Matching with Tuples in C#

This snippet demonstrates positional pattern matching in C# using tuples. Positional patterns allow you to deconstruct types based on their position, simplifying code and making it more readable, especially when dealing with data structures like tuples or custom classes with a defined deconstructor.

Basic Positional Pattern Matching

This example defines a method `GetShapeType` that takes a tuple representing the width and height of a shape. The `switch` expression uses positional patterns to match different shapes based on the values in the tuple. It first checks for a point (0, 0), then for a square (width equals height), and finally for rectangles with different width/height relationships. `var` keyword is used to capture the values and use them inside the `when` clause.

public static string GetShapeType((int Width, int Height) shape) =>
    shape switch
    {
        (0, 0) => "Point",
        (var w, var h) when w == h => "Square",
        (var w, var h) when w > h => "Rectangle (Width > Height)",
        (var w, var h) => "Rectangle (Width <= Height)",
    };

// Usage
Console.WriteLine(GetShapeType((0, 0))); // Output: Point
Console.WriteLine(GetShapeType((5, 5))); // Output: Square
Console.WriteLine(GetShapeType((10, 5))); // Output: Rectangle (Width > Height)
Console.WriteLine(GetShapeType((5, 10))); // Output: Rectangle (Width <= Height)

Concepts Behind Positional Pattern Matching

Positional pattern matching builds upon the concept of deconstruction. A type that supports deconstruction exposes a `Deconstruct` method. The pattern matching engine uses this method to extract the values from the object into the specified positions. In the case of tuples, deconstruction is automatically supported. For custom types, you need to define a `Deconstruct` method.

Custom Type Deconstruction

This example shows how to define a `Deconstruct` method for a custom class `Point`. This allows you to use positional pattern matching with `Point` objects. The `Deconstruct` method takes `out` parameters that correspond to the values you want to extract from the object. Discards (`_`) are used to ignore values that are not relevant for a particular pattern. It also demonstrates relational patterns, such as `> 0` and `< 0`.

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

public static string GetPointQuadrant(Point point) =>
    point switch
    {
        (> 0, > 0) => "Quadrant I",
        (< 0, > 0) => "Quadrant II",
        (< 0, < 0) => "Quadrant III",
        (> 0, < 0) => "Quadrant IV",
        (0, _) => "On Y-axis",
        (_, 0) => "On X-axis",
        _ => "Origin"
    };

// Usage
Console.WriteLine(GetPointQuadrant(new Point(1, 1))); // Output: Quadrant I
Console.WriteLine(GetPointQuadrant(new Point(0, 5))); // Output: On Y-axis
Console.WriteLine(GetPointQuadrant(new Point(0, 0))); // Output: Origin

Real-Life Use Case

Positional pattern matching is particularly useful when processing data from external sources, such as CSV files or databases, where the order of fields is well-defined. It can also simplify complex logic that involves multiple conditions based on the properties of an object. Consider parsing coordinates or geometrical data.

Best Practices

  • Keep Patterns Concise: Avoid overly complex patterns that are difficult to read and understand.
  • Use Discards Appropriately: Use discards (`_`) to ignore values that are not relevant for a specific pattern. This improves readability.
  • Consider Order of Patterns: The order of patterns in a `switch` expression matters. More specific patterns should come before more general patterns.
  • Ensure Deconstruction is Consistent: If you're using custom types, ensure the `Deconstruct` method returns values in a predictable and consistent order.

Interview Tip

Be prepared to explain the difference between positional and property patterns. Positional patterns rely on the order of values returned by a `Deconstruct` method (or are intrinsically defined like in a Tuple), while property patterns match based on the names of properties. Also, understand the role of the `Deconstruct` method in enabling positional pattern matching for custom types.

When to use them

Use positional patterns when:

  • The order of values is significant and well-defined.
  • You want to avoid creating multiple nested `if` statements.
  • You want to improve code readability and maintainability.

Alternatives

Alternatives to positional pattern matching include:

  • Nested `if` statements: Can become complex and difficult to read for intricate conditions.
  • Property patterns: Match based on property names instead of positions. Useful when the order of values is not important.

Pros

  • Improved Readability: Makes code more concise and easier to understand.
  • Reduced Boilerplate: Eliminates the need for manual property access and conditional checks.
  • Enhanced Expressiveness: Allows you to express complex logic in a declarative way.

Cons

  • Requires Deconstruction Support: For custom types, you need to define a `Deconstruct` method.
  • Can be less flexible than property patterns: If the order of values is not fixed, property patterns might be a better choice.

FAQ

  • What is a `Deconstruct` method?

    A `Deconstruct` method is a method that decomposes an object into its constituent parts. It takes `out` parameters that represent the values you want to extract from the object.
  • Can I use positional patterns with any type?

    You can use positional patterns with types that support deconstruction, either implicitly (like tuples) or explicitly (by defining a `Deconstruct` method).
  • What is the underscore `_` used for in positional patterns?

    The underscore `_` is a discard. It's used to indicate that you are not interested in a particular value in the pattern. It improves readability by making it clear which values are relevant.