NDepend

Improve your .NET code quality with NDepend

C# Pattern Matching Explained

C#-Pattern-Matching-Explained

Since the C# version 7, C# has support for pattern matching. C# pattern matching is here to simplify complex if-else statements into more compact and readable code. Pattern matching does not aim at writing code that cannot be written without. Its only purpose is to have more concise and elegant code.

Also keep in mind while reading that Pattern matching expressions are compiled to regular IL code. This point will be detailed later.

I believe the only way to present pattern matching in a proper and complete way is to explain its evolution through C# versions. Else this is cumbersome to illustrate what can be expressed and what cannot. The present post will be updated with future C# evolutions.

C#7: Null, Constant, Type, Discard and Var Patterns

C#7 introduced checking against a null pattern and a constant pattern.

One limitation of pattern matching – also till latest C# version- is that values embedded in pattern must be constant. In practice often the values to check against are not hardcoded but retrieved from some configurations. This limitation makes pattern matching useless in a number of situations.

Pattern-Matching-Requires-Constant

Notice that constant supported by patterns are:

  • an integer or floating-point numerical literal
  • a char or a string literal
  • a boolean value true or false
  • an enumeration value
  • the name of a declared const field or null

C#7 also introduced the type pattern, which is a great improvement, especially to introduce temporary variables in complex bool expressions:

Pattern matching works also with switch statements. This is especially useful to manage control flows through types that aren’t related by an inheritance hierarchy.

Notice the last default: clause that matches when seq is null. This is the discard pattern.

Notice also that the order of the case clauses matters. For example the compiler is smart enough to prevent such mistake:

Pattern-Matching-Clause-Order-Matters

Also with switch statement the keyword when can be used to add additional conditions that refine the pattern:

Notice that the keyword when cannot be used in if statement -only in switch statement – even with latest C# versions:

Pattern-Matching-when-cannot-be-used-in-if-expression

Finally C# 7 introduced the var pattern which is a special type pattern that matches even when null.

It is not recommended to use the var pattern to skip null check because undefined null state is dangerous. Practically the var pattern is used it in complex situations where anonymous types are involved.

C# 8: Switch Expressions and Property, Positional and Tuple Patterns

C#8 improved pattern matching in several ways. First there is switch expression:

Arguably the code is more readable because quite a lot of characters are saved here, no more case, no more variable declaration and bodies are expressions. Also compared to a regular expression switch, the compiler warns ( but don’t emit error) about not handled possible values. An exception is thrown at runtime when a switch expression reaches the end without a match.

Switch expression fits particularly well with expression-bodied member:

C#8 also introduced extremely useful property patterns.

Notice that:

  • The var pattern can be used to introduce variables to handle properties values.
  • A property pattern requires the object reference to be not null. Hence empty property pattern { } is a test for null check.

If rectangle has a deconstructor, the expression Rectangle { Width: var x, Height: var y } can be simplified to  Rectangle(var x, var y). This is the new C# 8 positional pattern.

Finally C#8 introduced the tuple pattern to test multiple values at the same time:

C# 9: Combinator, Parenthesized and Relational Patterns

C#9 introduced combinator patterns: conjunctive and, disjunctive or and negated not patterns. This is especially useful to avoid repeating a variable in a complex boolean expression.

The code sample above also illustrates the parenthesized pattern. Here parenthesis can be removed but they make the logic clearer.

Notice that the new negated not patterns constitutes a new syntax for null check: if(obj is not null) { ... }

C#9 also introduced relational patterns < > <= >=.

The relational pattern fits especially well when used with property pattern as shown by the code sample below. Also a nice C#9 addition illustrated below is that the underscore symbol can be omitted in type pattern for a lighter syntax:

C# 10: Extended Property Pattern

C# 10 introduced extended property pattern which is useful to nest property calls as illustrated by the code sample below:

C# 11: List and Slice Pattern [under development]

C#11 to be released in November 2022 might introduce new patterns like:

  • array is [1, 2, 3]  that will match an integer array of the length three with 1, 2, 3 as its elements, respectively.
  • [_, >0, ..] or [.., <=0, _] to match length >= 2 && ([1] > 0 || length == 3 || [^2] <= 0) where the length value of 3 implies the other test.
  • [_, >0, ..] and [.., <=0, _]to match length >= 2 && [1] > 0 && length != 3 && [^2] <= 0 where the length value of 3 disallows the other test.

See the public discussion here.

Pattern Matching is no magic

Pattern matching is no magic. At first glance one could think that a pattern expression is like a IQueryable LINQ expression: a language peculiarity that the compiler translates to a parametrized runtime object with special runtime processing. But it is not. Patterns are translated to traditional IL code. For example let’s decompile these two methods:

Here is the IL code:

Pattern-Matching-Decompiled

Don’t overuse Pattern Matching

C# Pattern matching is to complex if / else statements what C# LINQ is to for / foreach loop: a nice and modern C# improvement to write more concise and readable code. The parallel with C# LINQ can go a little further: the decompiled IL code above shows that pattern matching can emit more verbose code than regular if / else statements for similar behavior. Could pattern matching code be slower than the counterpart if / else code? There are a few evidences found on the web but no real thorough study. However the same recommendation applies: don’t use LINQ nor Pattern Matching in performance critical path executed millions or billions of time at runtime.

One of the most prominent pattern matching usage (at least in documentation) is type pattern. Concretely the code should behave differently if a shape is a circle or a rectangle. But one must keep in mind that polymorphism is here for that. It would be terrible design to use pattern matching to compute a shape area instead of providing an abstract Area property within the base class Shape:

This would be a maintenance nightmare since new kind of shape introduced would need its own area formula (a clear violation of the Open Close Principle). Also we can expect poor performance since virtual method table used at runtime to handle polymorphism is certainly faster than type compatibility checking.

Conclusion

Again C# Pattern Matching is to complex if / else statements what C# LINQ is to for / foreach loop: a nice and modern C# improvement to write more concise and readable code.

However pattern matching should be avoided in performance critical situations where usual checks can perform better at runtime. At least do benchmark such usage.

Type pattern is not a replacement for polymorphism and should be keep only for peculiar situations.

Finally – as underlined in the C# 7 section – C# pattern matching suffers from the limitation that only constant expressions can be used to match against. Maybe the C# team will relax this in the future but they need to find a good experience around exhaustiveness, particularly in switch expressions. With a non-compile-time-constant pattern, how the compiler could determine if all cases are handled? Another concern is when matching against values that trigger side effects – or even worse non-constant values – which would lead to undefined behavior.

 

 

 

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly and also did manage some academic and professional courses on the platform and C#.

Over my consulting years I built an expertise about the architecture, the evolution and the maintenance challenges of large & complex real-world applications. It seemed like the spaghetti & entangled monolithic legacy concerned every sufficiently large team. As a consequence, I got interested in static code analysis and started the project NDepend.

Today, with more than 12.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and full control on their application to a wide range of professional users around the world.

I live with my wife and our twin kids Léna and Paul in the beautiful island of Mauritius in the Indian Ocean.

Comments:

  1. Thanks for the clear summary! It’s nice to see how well C# is coming along, and I’m looking forward to playing around with the newer pattern types soon.

Comments are closed.