.NET Tools
Essential productivity kit for .NET and game developers
Recursive Pattern Matching – A Look at New Language Features in C# 8
Time 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:
- Indices, Ranges, and Null-coalescing Assignments
- Switch Expressions and Pattern-Based Usings
- Recursive Pattern Matching
- Async Streams
- Nullable Reference Types: Migrating a Codebase
- Nullable Reference Types: Contexts and Attributes
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:
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:
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:
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!