Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

How-To's

Recursive Pattern Matching – A Look at New Language Features in C# 8

ReSharper and Rider support for C# 8Time for another post in our C# 8 series! In this post, we will continue our journey through C# 8 language features, and dive into recursive pattern matching.

In this series, we are looking at:

Folks who have worked with functional languages will probably be familiar with the concept behind the recursive pattern matching C# 8 language feature. This technique can be used to validate and inspect objects more easily based on their shape.

Fundamental for patterns in C# is the is operator, which was extended only recently in C# 7 to also receive patterns additionally to type names for checking objects. Some patterns that we already know are type, constant, and var patterns. With C# 8, discard pattern, positional patterns and property patterns are being introduced.

Discard Pattern

Symbolized with _, the discard pattern matches just any expression. In switch expressions it can also be used as replacement for the default case:

var v = c switch
{
    1 => "One",
    _ => "anything else"
};

By the way, ReSharper and Rider will now also suggest to remove discard designations (C# 7) when they’re unused:
Remove discard

Deconstruction Pattern

The deconstruction pattern, or sometimes referred to as positional pattern, is used to check for null and to invoke a corresponding Deconstruct method to perform a positional deconstruction. Suppose we have a data type Point that can be deconstructed into its x and y coordinates:

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

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

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

We could then use positional patterns to either just capture the coordinates into a variable, or even to check them against other patterns, like constants:

string GetDisplayName(Point p) => p switch
{
    // Without deconstruction pattern
    _ when p.X == 0 && p.Y == 0 => "origin",
    _ => $"{p.X}/{p.Y}",

    // With deconstruction pattern
    (0, 0) => "origin",
    var (x, y) => $"{x}/{y}"
};

var p1 = new Point(0, 0);
var p2 = new Point(5, 10);

Console.WriteLine(GetDisplayName(p1)); // origin
Console.WriteLine(GetDisplayName(p2)); // 5/10

The deconstruction pattern also works with value tuples, so we could theoretically get rid of the Point type, and declare our method just like string GetDisplayName((int, int) p).

Of course, ReSharper and Rider will help us to get rid of common mistakes with various quick-fixes, for instance by removing type checks, fix component names, or remove component names altogether:
Remove type checks and fix component names

Object Pattern

One drawback of positional patterns, is that the order of deconstruction might not always be obvious. For instance, does a Person deconstruct into firstName and lastName, or lastName and firstName? A more flexible solution is described by the object pattern also known as property pattern, which works – as the name suggests – on the structure of an object. For further explanation, we can consider a more complex object structure:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public int ZipCode { get; set; }
}

And again, we can check for conditions and capture data:

// Prior to C# 8
if (c.LastName != null && c.Address.City != null && c.Address?.ZipCode == 99999)
    Console.WriteLine($"Hi {c.LastName} from {c.Address.City}");

// With C# 8
if (c is { LastName: { } lastName, Address: { City: { } city, ZipCode: 99999 } })
    Console.WriteLine($"Hi {lastName} from {city}");

As we can see from the comparison, property patterns are very flexible in their application. The { } pattern is basically checking, whether a variable or member is not null. We can further check their members, and their sub-members by simply continuing with the property pattern. The example from above also illustrates how different patterns can be used in combination. Here is another one, given we can deconstruct a Person into its lastName, firstName, and address:

if (c is ("Duck", var firstName, _) { Address: { City: var city, ZipCode: 99999 } })
    Console.WriteLine($"Hi {firstName} from {city ?? "nowhere"}");

And as you may have guessed, ReSharper and Rider also add some sugar when using property patterns, like code completion and quick-fixes:
Complete and fix member names

Download ReSharper Ultimate 2019.2 EAP or check out Rider 2019.2 EAP to start taking advantage of ReSharper’s language support updates and C# 8. We’d love to hear your thoughts!

image description