Back to All Posts

C# / .NET– Now you’re thinking with patterns 2022

Coming to this article, you probably thought this would be another post about design patterns in .NET. However, this is not one of them. Here, we want to explore another avenue introduced in the C# / .NET ecosystem, that being pattern matching.

Pattern matching is not a new concept in the programming world; however, it is more widespread in the space of functional programming languages such as Haskell or even C#’s cousin language F#. At its core, it is simply a way to test that an expression has certain characteristics and take an action as a result of a successful test.

In this article, we will be exploring the “patterns” of C#, diving into some implementation details where needed to gain a better understanding and see this technique’s evolution together with the language itself.

Constructs of Pattern Matching in C#

From a high-level perspective, the expressions and statements of C# that support and form the pattern matching syntax (at the time of writing) are the following:

  • expressions using the is operator
  • switch statements / expressions
  • logical operators not, and, or (work analogue to their !, &&, || counterparts)

If we are to take things historically, then switch statements are the first thing in C# that resembled pattern matching. While at that time, no one called it pattern matching, if we take a look in retrospective at its format, we can see the similarities.

 

 
Moving forward in time to 2017, the release of C# 7.0 marks the “official” introduction of pattern matching in the language, introducing the is keyword and basic patterns support for this new feature. Switch expressions support would come in C# 8 with additional improvements to pattern matching, which would then continue to get updates every new C# release (at least up to the time of writing).

Below, you will find a list to help you navigate the patterns we will go through:

Constant Patterns

Constant patterns were the first pattern introduced in C# 7.0, and as the name suggests, allow you to match values against other constants. This means that the types of values that you can use are rather restricted, being limited to “simple values” (like numeric values, enums, strings and null).

 

 
Above we can see what each type of constant pattern matching is lowered to (using SharpLab.io) and, for simple types and nullable types the is operator is being transformed into the same piece of code. For matching reference types against null, we see two possible transformations, with the first one being the same as previously mentioned with M2 being what we would expect at first from the other examples.

However, in the M1 case, there is a cast to object as the first step. This happens whenever the operator == is overloaded in our user defined type which would mean there is a possibility of the behavior being different and due to this, the C# compiler chooses to use the default reference equality check from object instead.

This leads to the conclusion that the constant pattern allows for a more fluent like syntax when checking for constants, preserving existing behavior in most cases, with the special case of ensuring that reference types are always checked for null as one would expect, even in the case of its == operator being overloaded with custom behavior.

Type Patterns

Type patterns can be seen in as a reflection alternative to runtime type checking of an object. However, one striking difference between them is that when using type pattern matching, only the compatibility of the types is being tested. This can lead to a looser type checking when dealing with hierarchies, demonstrated using the example below.

 

 
As we can see, when type matching against a base type, any derived type will cause a match, which can be useful depending on the use case (for example LINQ uses this internally to try and optimize some calls such as Count() on collections), but might catch you off-guard should you be unaware of this behavior.

Type patterns are usually combined with another ability of pattern matching, that being to capture the result in a case of a successful match. In this case, this can save the user of a manual safe-cast of the variable like shown below.

 

 

Relational & Logical Patterns

Relational patterns allow the use of relational operators >, , >=, = and logical pattern matching operators and, or, not in pattern matching together to create complex patterns. To showcase this pattern, we will make use of a switch expression to get the current season based on a date.

 

 
With this example we also showcased how multiple patterns can be combined to obtain a desired result. One thing noteworthy is the precedence of the logical pattern matching operators which is maintained as for the regular logical C# operators with not having the highest priority, then and, then or, which is best shown on the first branch of the switch expression in the example.

Property Patterns

Property patterns arrived in C# 8.0 and are what we would consider, the subset that has the most potential from all the pattern matching techniques. They allow you to evaluate an object based on the values of its properties. This is also applicable for nested properties that can be tested to achieve patterns like exemplified below.

 

 
The example illustrates the power of the property pattern by displaying the amount of space saving achievable through using pattern matching when compared to the regular if checks. First of all, we start by having built in null safety checking built into property patterns. Then, we gain the ability to “declaratively” specify how an object matching our criteria should look like and all the hassle of manually checking those properties is abstracted away by the compiler.

Another more niche / preference use case for them, is another way of checking if an object is null or not by comparing it with the empty property pattern. This can be seen demonstrated below (left equivalent to right).

 

 

Deconstruct Patterns

Deconstruct patterns (in some places they are also called positional patterns) take advantage of the deconstruction mechanism for objects present in C#. As a quick refresher, if an object has a void method called Deconstruct without parameters, it can automatically be deconstructed in a tuple in a single operation.

Pattern matching is available on tuples as with any other type, and it is aware of the deconstruction mechanism so it allows us to match the object based on the deconstructed tuple directly.

 

 
As we can see, we can match on complexObject directly as if it were a tuple type. Moreover, previously mentioned pattern matching techniques and variable binding are fully available inside tuples. In the example above, we are matching on an object where oneProp is a string with at least 4 characters, anotherProp can be any integer and we are binding both to the variables x and y to be ready to use inside the if block.

Also based on the tuples ability to be matched element by element, we can also derive a nifty way to sometimes make regular conditions shorter and more concise.

 

 
Since we can construct value tuples at will by combining different elements, we can also combine the conditions inside a single pattern check where possible. This has the same end result and since value tuples are cheap to create, this can be an alternative based on your preference.

List Patterns

List patterns are newest addition to the pattern matching suite, being introduced with the release of C# 11 and .NET 7. As you can probably deduce from the name, they allow you to match lists against patterns.

Let’s start with a simple example and understand the basics.

 

 
Following the example above we can see that it is possible to match individual elements of a list capturing them in variables if needed, and even test further patterns on each element of the list (like in the 4th branch of the switch).

This can be useful if you know the exact dimensions of a list, however you are probably wondering, “what if I’m not interested in every element and just want the first or the last” or “what if I don’t know the exact size of the list and I don’t want to write a branch for each possible size”.

Those are all perfect questions and here is where a bit of confusion comes into play. Even if the official name of the technique is called list patterns, to take advantage of the entire feature set that would answer the previously mentioned questions, we cannot use lists there, we have to use collections that support slicing, with the main candidate being Arrays instead of lists, as internally the slice operation is used for those cases. Let’s have another example to demonstrate what can be achieved on arrays.

 

 
If we follow the example above, we immediately see a new syntax being introduced, that being the “..” syntax. Using it we can collect multiple elements in the initial list in one pattern and even apply further tests to the pattern. The only restriction we have is that slicing inside a pattern can only be used once, however as we can see from the example its use is very flexible as it accommodates any scenario that is logically correct. Taking advantage of slicing turns “array patterns” (to call them exactly what they are) into a potentially extremely powerful feature.

One thing we have to be careful of when using slicing, is that every new slice that we make, represents an entirely new array. This new array is mutable which depending on the use case might be what is needed; however, we would expect that most use cases for this will be operations without side effects, even on the sliced arrays.

This is important as every new allocation costs, which might impact performance if used repeatedly (maybe even recursively) on a large enough array. If you need to use slicing but all your operations have no side effects on the sliced arrays, then it is possible to eliminate the overhead of new allocations by using Span instead of T[] (or call AsSpan() before matching).

Final Words

We have described many of the pattern matching techniques in C# introduced over the years as this feature set evolved. While we have certainly proven that it is powerful when used in the right circumstances, the fact that most of the techniques interact with each-other and allow “recursion-like” behavior inside patterns is both a blessing and a curse.

One on hand, the power to combine multiple patterns can ease the number of checks we have to do, falling into the other side where we try and match too much in a single go, can lead to severe reduction in readability of the exact criteria we are searching for. As a (hopefully) fun exercise we’ve left a trivia like question below with one of these cases where we went maybe a bit too far with the checks.

 

After all, we hope you will find your match in at least one of the techniques previously presented.