In my
earlier article on Pattern Matching, we explored different pattern matching abilities and how it vastly improved the written code. In C# 9, the pattern matching was further improved upon with the introduction of a couple of patterns.
Simple Type Pattern
Let us begin by writing a sample Type pattern from C# 8.0.
- public string EvaluateSwitchExpression(T criteria) => criteria switch
- {
- Int32 value => $"Type {nameof(Int32)}, Value = {criteria}",
- Int64 _ => $"Type {nameof(Int64)}, Value = {criteria}",
- string value => $"Type {nameof(String)}, Value = {criteria}",
- List<int> value when value.Count < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",
- List<int> {Count:5 } value => $"Type Medium {nameof(List<int>)}, Value = {value}",
- List<int> value => $"Type Big {nameof(List<int>)}, Value = {value}",
- null => "Null Detected",
- _ => $"Type Unknown"
- };
While the switch expression has made code a lot more lean together with the patterns introduced by C# 8.0, there was still a bit of boiler plate code we liked to avoid. As seen in the above code, the type pattern required us to use a identifier, even if it is a discard. C# 9.0 helps us to remove this little bit of boiler code.
We could now rewrite the above code as.
- public string EvaluateSwitchExpression(T criteria) => criteria switch
- {
- Int32 => $"Type {nameof(Int32)}, Value = {criteria}",
- Int64 => $"Type {nameof(Int64)}, Value = {criteria}",
- String => $"Type {nameof(String)}, Value = {criteria}",
- List<int> value when value.Count < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",
- List<int> { Count: 5 } => $"Type Medium {nameof(List<int>)}, Value = {criteria}",
- List<int> => $"Type Big {nameof(List<int>)}, Value = {criteria}",
- null => "Null Detected",
- _ => $"Type Unknown"
- };
That does make the code a lot easier to read.
Relational Pattern
If you examine the code above, there is still a particular set of cases which you would probably like to improve.
- List<int> value when value.Count < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",
- List<int> { Count: 5 } => $"Type Medium {nameof(List<int>)}, Value = {criteria}",
- List<int> => $"Type Big {nameof(List<int>)}, Value = {criteria}",
In the above code written in C# 8.0, we are checking 3 conditions when T is List<int>.
- when number of items in List is less than 5, return 'Small'
- when number of items in List is 5, return 'Medium'
- when number of items in List is more than 5, return 'Big'
If we look at the code again, we can see that the readability could be improved. This can be done with Relational Pattern in C# 9, which allows us to use relational operators in our patterns. For example,
- public string EvaluateSwitchExpression(T criteria) => criteria switch
- {
- Int32 => $"Type {nameof(Int32)}, Value = {criteria}",
- Int64 => $"Type {nameof(Int64)}, Value = {criteria}",
- String => $"Type {nameof(String)}, Value = {criteria}",
- List<int> value => value.Count switch
- {
- < 5 => $"Type Small {nameof(List<int>)}, Value = {value}",
- > 5 => $"Type Big {nameof(List<int>)}, Value = {value}",
- _ => $"Type Medium {nameof(List<int>)}, Value = {value}",
-
- },
- null => "Null Detected",
- _ => $"Type Unknown"
- };
The inner switch expression matches relational operators, in our scenario <5 and >5 using the relational pattern.
Logical Pattern
We have already seen usage of relational operator in pattern matching. The logical pattern allows you to use logical operators with patterns. For example, for the above scenario, let us modify the above conditions as:
- when number of items in List is less than 5, return 'Small'
- when number of items in List is 5, return 'Medium'
- when number of items in List is more than 5 AND less than 10, return 'Big',
- when number of items in List is more than 10, return 'Extra Large'
- We could now rewrite the expression
- public string EvaluateSwitchExpression(T criteria) => criteria switch
- {
- Int32 => $"Type {nameof(Int32)}, Value = {criteria}",
- Int64 => $"Type {nameof(Int64)}, Value = {criteria}",
- String => $"Type {nameof(String)}, Value = {criteria}",
- List<int> value => value.Count switch
- {
- < 5 => $"Type Small {nameof(List<int>)}, Value = {value.Count}",
- > 10 => $"Type Extra Large {nameof(List<int>)}, Value = {value.Count}",
- > 5 and < 10 => $"Type Big {nameof(List<int>)}, Value = {value.Count}",
- _ => $"Type Medium {nameof(List<int>)}, Value = {value.Count}",
- },
- null => "Null Detected",
- _ => $"Type Unknown"
- };
We can also improve the last two cases, where we have used discard to indicate not null by using logical pattern and operator null. For example,
- null => "Null Detected",
- _ => $"Type Unknown"
The above can be replaced with:
- null => "Null Detected",
- not null => $"Type Unknown"
The complete code could be now rewritten as:
- public string EvaluateSwitchExpression(T criteria) => criteria switch
- {
- Int32 => $"Type {nameof(Int32)}, Value = {criteria}",
- Int64 => $"Type {nameof(Int64)}, Value = {criteria}",
- String => $"Type {nameof(String)}, Value = {criteria}",
- List<int> value => value.Count switch
- {
- < 5 => $"Type Small {nameof(List<int>)}, Value = {value.Count}",
- > 10 => $"Type Extra Large {nameof(List<int>)}, Value = {value.Count}",
- > 5 and < 10 => $"Type Big {nameof(List<int>)}, Value = {value.Count}",
- _ => $"Type Medium {nameof(List<int>)}, Value = {value.Count}",
- },
- null => "Null Detected",
- not null => $"Type Unknown"
- };
That is definetly more readable. You can also use the not with if conditions. For example,
As you can observe, the pattern matching continues to improve with language. We can expect more from it as the language evolves in future versions.