Revisiting Pattern Matching And Switch Expressions

Introduction 

 
C# 7 introduced us to pattern matching and we have been falling in love with it so much that we didn't realize it was nonexistent prior to C#. Before we delve into the new patterns introduced in C# 8, let's take a quick recap of the pattern introduced in C# 7.
 

Const Pattern

 
The constant pattern extends the is statement to compare the expression to a constant. For example:
  1. if(input is 7m)  
  2. {  
  3.   

or
  1. const decimal VALUE = 7m;  
  2. if(input is VALUE)  
  3. {  
  4.   

The most common use case of the Constant Pattern is for checking null.
  1. if(input is null)  
  2. {  
  3.   

var Pattern


Probably the least used among the patterns introduced in C# 7 is the var pattern. The var pattern is unique in its own way. It always succeeds.

So, what would be the use-case for something that always succeeds? Since the pattern always succeeds and the value is assigned to the variable, you could use the var pattern to create a temporary variable.

This is especially useful inside a linq query where you could create a temporary variable inside a where condition. For example:
  1. var inputCollection = Enumerable.Range(1,100)      
  2.                                 .Select(x=> Enumerable.Range(x,10).Select(c=>c*x));      
  3. var result = inputCollection.Where(x=> x.ToList() is var list      
  4.                                 && list.Average() > 100);     
 The var pattern is also useful inside the switch statement, especially when used with a case guard. For example:
  1. private string Evaluate(int input)  
  2. {  
  3.     var inputCollection = Enumerable.Range(1,100)  
  4.                                      .Select(x=> Enumerable.Range(x,10).Select(c=>c*x));  
  5.     switch(input)  
  6.     {  
  7.         case 5:  
  8.             return "Five";  
  9.         case 4:  
  10.             return "Six";  
  11.         case var v when inputCollection.Any(x=>x.Contains(v)):  
  12.             return "Collection Contains the value";  
  13.         default :  
  14.             return "Not Found";  
  15.     }  

Type Pattern


The most common pattern used would be the Type Pattern, which checks if the expression matches a type and if so, converts it to a particular type.
  1. if(input is double val)  
  2. {  
  3.     // code use val  

Type patterns could also be used within a Switch Statement. For example: 
  1. public string EvaluateSwitchStatement(T criteria)  
  2. {  
  3.     switch (criteria)  
  4.     {  
  5.         case Int32 value : return $"Type {nameof(Int32)}, Value = {value}";  
  6.         case Int64 value: return $"Type {nameof(Int64)}, Value = {value}";  
  7.         case string value: return $"Type {nameof(String)}, Value = {value}";  
  8.         case List<int> value when value.Count < 5: return $"Type Small {nameof(List<int>)}, Value = {value}";  
  9.         case List<int> value when value.Count == 5: return $"Type Medium {nameof(List<int>)}, Value = {value}";  
  10.         case List<int> value: return $"Type Big {nameof(List<int>)}, Value = {value}";  
  11.         case nullreturn "Null Detected";  
  12.         default:  return $"Type Unknown";  
  13.     }  

As one can observe, the patterns have made the switch statements whole the more powerful. However, there is a lot of boilerplate code going around. You would wish to do away with the repeated case and return/breaks.
 

Introducing Switch Expressions

 
Before we look into other patterns, it would be a good idea to introduce one of the finest features of C# 8 - the switch expressions.

The switch expression introduces a switch like syntax in the context of expression and provides a clean and concise way for writing switch when each switch arm produces a value.

Let us rewrite the switch statement in our example of type pattern using a switch expression.
  1. public string EvaluateSwitchExpression(T criteria) => criteria switch  
  2. {  
  3.     Int32 value => $"Type {nameof(Int32)}, Value = {value}",  
  4.     Int64 value => $"Type {nameof(Int64)}, Value = {value}",  
  5.     string value => $"Type {nameof(String)}, Value = {value}",  
  6.     List<int> value when  value.Count < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",  
  7.     List<int> value when value.Count == 5 =>  value => $"Type Medium {nameof(List<int>)}, Value = {value}",  
  8.     List<int> value => $"Type Big {nameof(List<int>)}, Value = {value}",  
  9.     null => "Null Detected",  
  10.     _ => $"Type Unknown"  
  11. }; 
As one can observe, the syntax has become leaner. You do not have to use the repeated return statements or breaks that associated with the switch statement.
 
The syntax has changed ever so slightly. The syntax now ensures the switch arms consists of:
  • Pattern
  • Optional Case Guard
  • The => token
  • Expression.
The criteria in the switch expression are known as Range Expression. The noticeable change is how the switch keyword now succeeds in the range expression.
 
The code above also introduces the discard pattern. Discard pattern - matches all expressions and is used to matching the default in switch expression. And since it matches all expression, it needs to appear at the very end of the switch expression.
 
Now that's all only one side of the story. The following patterns which were introduced in C# 8, make the switch expressions even more powerful. Let's go ahead and explore them.
 

Property Pattern

 
The property pattern enables you to check if the given value is null and match the public properties on the object. For example:
  1. public class Foo  
  2. {  
  3.     public string FirstName {get;set;}  
  4.     public string LastName {get;set;}  
  5. }  
  6.   
  7. var foo = new Foo {FirstName = "Anu", LastName="Viswan"};  
  8. if(foo is Foo {FirstName:"Anu",LastName:"Viswan"})  
  9. {  
  10.    // code  

The property pattern allows you to check if Foo.FirstName and Foo.LastName matches the desired values. Compare this lean syntax against the conventional syntax which existed before the patterns were introduced.
  1. if(foo is Foo && foo.FirstName=="Anu" && foo.LastName=="Viswan")  
  2. {  
  3.     // code  

As evident from the code above, the property pattern has made the code more concise.
 
We could use the property pattern in our preceding example of switch expression to trim it down further.
  1. public string EvaluateSwitchExpression(T criteria) => criteria switch  
  2. {  
  3.     Int32 value => $"Type {nameof(Int32)}, Value = {value}",  
  4.     Int64 value => $"Type {nameof(Int64)}, Value = {value}",  
  5.     string value => $"Type {nameof(String)}, Value = {value}",  
  6.     List<int> value when  value.Count < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",  
  7.     List<int> {Count:5 }  value => $"Type Medium {nameof(List<int>)}, Value = {value}",  
  8.     List<int> value => $"Type Big {nameof(List<int>)}, Value = {value}",  
  9.     null => "Null Detected",  
  10.     _ => $"Type Unknown"  
  11. }; 
The highlighted expression has been made more concise, eliminating the case guard instead of using the property pattern. Let's explore one more case of property pattern where the pattern is exclusively used.
  1. public class Person  
  2. {  
  3.     public string FirstName { getset; }  
  4.     public string LastName { getset; }  
  5.     public int Age { getset; }  
  6. }  
  7.   
  8. public string EvaluateSwitchExpression(Person criteria) => criteria switch  
  9. {  
  10.     { FirstName: "Anu", Age: 36 } => "FirstName and Age Matched",  
  11.     { LastName: "Viswan" } person when person.Age < 40 => "LastName With Age Less than Matched",  
  12.     { LastName: "Doe" } => "LastName Matched",  
  13.     null => "Input was null"  
  14.     _ => "Not Found"  
  15. }; 
We could make a small little improvement here towards writing more concise code. If your input was a non-null and one that doesn't satisfy any of the first 3 cases, then you would end up in the fallback case of discard pattern. Instead, to be more precise, you could use '{}' to denote any non-null value. Here, we're rewriting our switch with the improvement:
  1. public string EvaluateSwitchExpression(Person criteria) => criteria switch  
  2. {  
  3.     { FirstName: "Anu", Age: 36 } => "FirstName and Age Matched",  
  4.     { LastName: "Viswan" } person when person.Age < 40 => "LastName With Age Less than Matched",  
  5.     { LastName: "Doe" } => "LastName Matched",  
  6.     { } => "Not Found",  
  7.     null => "Input was null"  
  8. }; 
The switch is still exhaustive and yet more concise.
 

Positional Pattern

 
What if as a developer, we had the option to write Deconstruct for the type Person. That would open doors for us to improve the earlier switch expression further by using the Positional Pattern.
 
Let us go ahead and write the Deconstruct for Person first before updating our switch expression.
  1. public class Person  
  2. {  
  3.     public string FirstName { getset; }  
  4.     public string LastName { getset; }  
  5.     public int Age { getset; }  
  6.     public void Deconstruct(out string firstName, out string lastName, out int age) => (firstName, lastName, age) = (FirstName, LastName, Age);  

Now with the Deconstruct in place, let us rewrite our switch expression using the positional pattern.
  1. public string EvaluateSwitchExpression(Person criteria) => criteria switch  
  2. {  
  3.     ("Anu", _, 36 )=> "FirstName and Age Matched",  
  4.     (_,"Viswan",_) person when person.Age < 40 => "LastName With Age Less than Matched",  
  5.     (_,"Doe",_) => "LastName Matched",  
  6.     { } => "Not Found",  
  7.     null => "Input was null"  
  8. }; 
Gone are the property names which were required in the Property pattern. The code is even cleaner now. The positional pattern is not restricted to switch expression or switch statement, you could also use it in the regular if conditions. For example:
  1. if(person is ("Anu","Viswan",36))  
  2. {  
  3.   

Tuple Pattern

 
If the Deconstruct could lead us to the positional pattern, then the Tuple could not be left behind either. The Tuple Pattern is a special case of a positional pattern that makes use of the Tuples.
 
The Tuple Pattern comes across as a great tool when one has multiple criteria to test. Consider the following scenario:
  1. public string EvaluateSwitchExpression(string firstName, string lastName, int age) => (firstName, lastName, age) switch  
  2. {  
  3.     ("Anu""Viswan", _) => "FirstName and Age Matched",  
  4.     (_, "Viswan", var a) when a < 40 => "LastName With Age Less than Matched",  
  5.     (_, "Doe", _) => "LastName Matched",  
  6.     (nullnull, _) => "Input was null",  
  7.     (_, _, _) => "Not Found",  
  8. }; 

Summary

 
Clearly, the evolution of Pattern Matching and introduction of switch expression has made it possible to write more concise code. This evolution of the language has made it more exciting times for .NET developers in times ahead.


Similar Articles