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.
1 2 3 4 5 6 7 8 |
static void NullPattern(object o, int? ni) { if (o is null) Console.WriteLine("o is null"); if (!(ni is null)) Console.WriteLine(ni.Value); } static void ConstantPattern(double d, string str) { if (d is Math.PI) Console.WriteLine("d is PI"); if (str is "314159") Console.WriteLine("str looks like PI"); } |
One limitation of pattern matching – also till the latest C# version- is that values embedded in a 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.
Notice that constant supported by patterns are:
- an integer or floating-point numerical literal
- a char or a string literal
- a boolean value
true
orfalse
- an enumeration value
- the name of a declared
const
field ornull
C#7 also introduced the type pattern, which is a great improvement, especially to introduce temporary variables in complex bool expressions:
1 2 3 4 5 6 7 8 |
static void TypePattern(Shape shape) { if (shape is Circle circle1) Console.WriteLine($"shape is a circle of radius {circle1.Radius}"); // Type pattern and compound expressions if (shape is Rectangle rect && rect.Width == rect.Height) Console.WriteLine($"shape is a square of length {rect.Width}"); } |
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.
1 2 3 4 5 6 7 8 9 10 11 |
public static int Count<T>(this IEnumerable<T> seq) { switch (seq) { case Array a: return a.Length; case ICollection<T> c: return c.Count; case IReadOnlyCollection<T> c: return c.Count; // Matches if seq is not null case IEnumerable<T> _: return seq.Count(); // Discard pattern when seq is null default: return 0; } } |
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, since the class List<T>
implements the interface ICollection<T>
, the compiler is smart enough to prevent such mistake:
Also with switch statement the keyword when
can be used to add additional conditions that refine the pattern:
1 2 3 4 5 6 7 8 |
public static int CountUpTo10<T>(this IEnumerable<T> seq) { switch (seq) { case Array a when a.Length <= 10: return a.Length; case ICollection<T> c when c.Count <= 10: return c.Count; case IReadOnlyCollection<T> c when c.Count <= 10: return c.Count; default: throw new ArgumentException("Too large sequence"); } } |
Notice that the keyword when
cannot be used in if
statement -only in switch
statement – even with the latest C# versions:
Finally C# 7 introduced the var pattern which is a special type pattern that matches even when null.
1 2 3 4 5 |
static void VarPattern() { object o = null; Assert.IsFalse(o is object); Assert.IsTrue(o is var v); } |
It is not recommended to use the var pattern to skip null check because an undefined null state is dangerous. Practically the var pattern is used 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 a switch expression:
1 2 3 4 5 6 7 |
static void SwitchExpression(Shape shape) { string whatShape = shape switch { Circle c => $"shape is a circle of radius {c.Radius}", Rectangle _ => "shape is a rectangle", _ => "shape is null or not a known type of shape" }; } |
Arguably the code is more readable because quite a lot of characters are saved here, no more case
, no more variable declarations and bodies are expressions. Also compared to a regular expression switch
, the compiler warns (but doesn’t emit an 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 members:
1 2 3 4 5 |
public static string WhatShape(this Shape shape) => shape switch { Circle r => $"This is a circle of radius {r}", Rectangle _ => "This is a rectangle", _ => "shape is null or not a known type of shape" }; |
C#8 also introduced extremely useful property patterns.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void PropertyPattern(Shape shape) { if(shape is Circle { Radius: 1.0d }) Console.WriteLine($"shape is a circle of radius 1"); string whatShape = shape switch { Circle { Radius: 1.0d } => "shape is a circle of radius 1", Rectangle r when r.Width == r.Height => "shape is a square", Rectangle { Width: 10d, Height: 5d} => "shape is a 10 x 5 rectangle", Rectangle { Width: var x, Height: var y } => $"shape is a {x} x {y} rectangle", { } => "shape is not null", _ => "null" }; } |
Notice that:
- The var pattern can be used to introduce variables to handle property values.
- A property pattern requires the object reference to be not null. Hence empty property pattern
{ }
is a test for null check.
If the 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Rectangle : Shape { public double Height { get; set; } public double Width { get; set; } public override double Area => Height * Width; public void Deconstruct(out double width, out double height) { width = Width; height = Height; } } public static string WhatShapeWithPositionalPattern(this Shape shape) => shape switch { Circle r => $"This is a circle of radius {r}", Rectangle(var x, var y) => $"shape is a {x} x {y} rectangle", _ => "shape is null or not a known type of shape" }; |
Finally C#8 introduced the tuple pattern to test multiple values at the same time:
1 2 3 4 5 6 7 8 |
public static string ColorsOfWithTuplePattern(Color c1, Color c2, Color c3) => (c1, c2, c3) switch { (Color.Blue, Color.White, Color.Red) => "France", (Color.Green, Color.White, Color.Red) => "Italy", (Color.Black, Color.Red, Color.Yellow) => "Germany", _ => throw new ArgumentException("Unknown flag") }; } |
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.
1 2 |
public static bool IsLetterOrSeparator(this char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','; |
The code sample above also illustrates the parenthesized pattern. Here you can remove the parenthesis but they make the logic clearer.
Notice that the new negated not
pattern constitutes a new syntax for null check: if(obj is not null) { ... }
C#9 also introduced relational patterns < > <= >=
.
1 2 3 4 5 6 |
public static double GetTaxRateWithRelationalPattern(double monthlyIncome) => monthlyIncome switch { >= 0 and < 1000 => 0, < 5000 => 10, _ => 20 }; |
The relational pattern fits especially well when used with a property pattern as shown by the code sample below. Also, this example illustrates a nice C#9 addition: you can omit the underscore symbol in type pattern for a lighter syntax:
1 2 3 4 5 6 7 8 9 10 11 |
public static string WhichShapeWithRelationalPattern(this Shape shape) => shape switch { Circle { Radius: > 1 and <= 10} => "shape is a well sized circle", Circle { Radius: > 10 } => "shape is a too large circle", // Before C#9 underscore was required: Rectangle _ => Rectangle => "shape is a rectangle", // Here underscore is still required _ => "shape doesn't match any pattern" }; |
C# 10: Extended Property Pattern
C# 10 introduced an extended property pattern which is useful to nest property calls as illustrated by the code sample below:
1 2 3 4 5 6 7 8 |
public static IEnumerable<Person> PersonsWithShortNameCSharp9(this IEnumerable<object> seq) => seq .Where(x => x is Person { FirstName: { Length: <= 5 } }) .Cast<Person>(); public static IEnumerable<Person> PersonsWithShortNameCSharp10(this IEnumerable<object> seq) => seq .Where(x => x is Person { FirstName.Length: <= 5 }) .Cast<Person>() |
C# 11: List and Slice Pattern
C#11 introduced list pattern matching. For example array is [1, 2, 3]
will match an integer array of the length three with 1, 2, 3 as its elements, respectively. Notice that:
- The new slice pattern
..
matches any sequence of zero or more elements. - The discard pattern
_
matches any single element.
Consequently:
- the pattern
[_, >0, ..]
meansarray.Length >= 2 && array[1] > 0
. - the pattern
[.., <=0, _]
meansarray.Length >= 2 && array[^2] <= 0
.
Notice that the slice pattern can only appear at most once in a list pattern. Two slice patterns cannot check if an array contains a particular item:
List pattern matching can be useful to introduce variables like in this method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
double Sum(ReadOnlySpan<double> vector) { switch (vector) { case []: return 0; case [double x]: return x; case [double x, double y]: return x + y; default: double sum = 0; for (int i = 0; i < vector.Length; ++i) { double v = vector[i]; sum += v; } return sum; }; } |
The input of the list pattern matching can be any object that is both countable (with a property Length
or count
) and indexable (supporting the syntax [int]
). For example, it can work on a string
. When dealing with your custom types, you can enable them to function as inputs for list patterns by providing the required members.
1 2 3 4 5 6 7 8 9 10 |
string CountryCode(string s) { switch (s) { case [char c0, char c1]: // Really test that (s.Length == 2) return c0 == 'F' && c1 == 'R' ? "France" : c0 == 'U' && c1 == 'S' ? "USA" : "Other"; default: return "Unknown"; }; } |
List pattern matching is a recursive pattern. This means that elements of the list test can also be lists like for example:
1 2 3 |
bool ContainsAtLeastOneListAndEndWithASingleIntList(List<List<int>> listOfList) { return listOfList is [.., [_]]; } |
Finally, notice that the var pattern can be used to check some patterns on the sub-array of a slice pattern like in the expression [.. var nMinusOneFirstElems, 37]
. However, doing so is not recommended because this can lead the compiler to allocate a new array just for the check:
The same way a string can be allocated:
C# 12: No new Pattern Matching syntax
C# 12 didn’t introduce new Pattern Matching syntax. See the list of C# 12 new features 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:
1 2 3 4 5 6 |
public static bool IsLetterOrSeparator1(this char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','; public static bool IsLetterOrSeparator2(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','; |
Here is the IL code:
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:
1 2 3 4 |
public static double Area(this Shape shape) => shape switch { Circle c => c.Radius * c.Radius * Math.PI, Rectangle r => r.Width * r.Height }; |
This would be a maintenance nightmare since a 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 the 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, you should avoid pattern matching in performance-critical situations where usual checks can perform better at runtime. At least do benchmark such usage.
Also, type pattern is not a replacement for polymorphism. You can keep it 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 the expression handles all cases? Another concern is when matching against values that trigger side effects – or even worse non-constant values – which would lead to undefined behavior.
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.