In this blog post we’re going to be looking at one of the features released in C# 8.0, supported on .NET Core 3.x and .NET Standard 2.1. I’m talking about the latest improvements done to pattern matching. Pattern matching, a core concept in software development, is the evaluation of data during run time and basing the flow of the program based on that outcome. A common way to do this in C# is to use an if or a switch statement, which I’m sure you did if you’re reading this blog post.
This was originally introduced in C# 7.0 with the introduction of the is keyword. The is keyword was introduced to evaluate for type patterns (checks an object’s type), constant patterns (compares a value to a constant) and var patterns (match that always succeeds and binds the value of an expression to a new local variable). C# 8.0 adds new functionality to the current syntax and techniques.
Switch Expressions
Starting with the introduction of switch expressions, up until C# 8.0 this is how we would normally make use of a switch statement.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSport(string ballSize) | |
| { | |
| switch (ballSize) | |
| { | |
| case "tiny": | |
| return SportType.Golf; | |
| case "small": | |
| return SportType.Tennis; | |
| case "medium": | |
| return SportType.Volleyball; | |
| case "large": | |
| return SportType.Football; | |
| case "extra large": | |
| return SportType.Basketball; | |
| default: | |
| throw new ArgumentException("invalid ball properties"); | |
| } | |
| } |
We have a method that checks the value of the string variable, representing a ball size, and returns an enum, representing a sport, based on the following enum.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public enum SportType | |
| { | |
| Golf, | |
| Tennis, | |
| Volleyball, | |
| Football, | |
| Basketball, | |
| AmericanFootball, | |
| Rugby | |
| } |
The following is the same implementation as above but with the new switch expressions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSport(string ballSize) | |
| { | |
| return ballSize switch | |
| { | |
| "tiny" => SportType.Golf, | |
| "small" => SportType.Tennis, | |
| "medium" => SportType.Volleyball, | |
| "large" => SportType.Football, | |
| "extra large" => SportType.Basketball, | |
| _ => throw new ArgumentException("invalid ball properties") | |
| }; | |
| } |
The main differences are
- The evaluating variable comes before the switch keyword. An easy way to distinguish between a switch statement and a switch expression.
- The case and : syntax elements are replaced with =>.
- The default case is replaced with a _ discard. Introduced in C# 7.0
- The bodies are expressions not statements.
Property Patterns
In property patterns we’ll now look at how to evaluate the value of a property inside an object. Let’s consider the following class, based on the same sport/ball size example. For the time being ignore the property BallShape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class Sport | |
| { | |
| public string BallSize { get; set; } | |
| public string BallShape { get; set; } | |
| public SportType Name { get; set; } | |
| public Sport(string size, string shape) | |
| { | |
| BallSize = size; | |
| BallShape = shape; | |
| } | |
| } |
Based on the above Sport class, if we had to evaluate the BallSize property inside a switch expression, this is how it will look.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSport(Sport sport) | |
| { | |
| return sport switch | |
| { | |
| { BallSize: "tiny" } => SportType.Golf, | |
| { BallSize: "small" } => SportType.Tennis, | |
| { BallSize: "medium" } => SportType.Volleyball, | |
| { BallSize: "large" } => SportType.Football, | |
| { BallSize: "extra large" } => SportType.Basketball, | |
| _ => throw new ArgumentException("invalid ball properties") | |
| }; | |
| } |
The switch expression provides a cleaner and shorter solution to the traditional switch statement applied to an object’s property, which for comparison I have added below.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSportPropertyOld(Sport sport) | |
| { | |
| switch (sport.BallSize) | |
| { | |
| case "tiny": | |
| return SportType.Golf; | |
| case "small": | |
| return SportType.Tennis; | |
| case "medium": | |
| return SportType.Volleyball; | |
| case "large": | |
| return SportType.Football; | |
| case "extra medium": | |
| return SportType.Basketball; | |
| default: | |
| throw new ArgumentException("invalid ball properties"); | |
| } | |
| } |
Tuple Patterns
Finally, we are going to introduce another value to evaluate and this will open up more patterns and options. Besides checking for the ball size, we’ll also be checking for the ball shape. A tuple is the ideal type to use as it holds two values, and this is how you would evaluate a tuple using a switch expression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSport((string, string) ballProperties) | |
| { | |
| return ballProperties switch | |
| { | |
| ("tiny", "round") => SportType.Golf, | |
| ("small", "round") => SportType.Tennis, | |
| ("medium", "round") => SportType.Volleyball, | |
| ("large", "round") => SportType.Football, | |
| ("extra large", "round") => SportType.Basketball, | |
| ("medium", "oval") => SportType.AmericanFootball, | |
| ("large", "oval") => SportType.Rugby, | |
| _ => throw new ArgumentException("invalid ball properties") | |
| }; | |
| } |
Again, for clarification, this was possible before but I’m sure you would agree with me saying that it wasn’t the cleanest solution. Here’s the more traditional approach.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| private static SportType GetSport((string, string) ballProperties) | |
| { | |
| switch (ballProperties) | |
| { | |
| case var b when b == ("tiny", "round"): | |
| return SportType.Golf; | |
| case var b when b == ("small", "round"): | |
| return SportType.Tennis; | |
| case var b when b == ("medium", "round"): | |
| return SportType.Volleyball; | |
| case var b when b == ("large", "round"): | |
| return SportType.Football; | |
| case var b when b == ("extra large", "round"): | |
| return SportType.Basketball; | |
| case var b when b == ("medium", "oval"): | |
| return SportType.AmericanFootball; | |
| case var b when b == ("large", "oval"): | |
| return SportType.Rugby; | |
| default: | |
| throw new ArgumentException("invalid ball properties"); | |
| } | |
| } |
That’s it in terms of pattern matching in C# 8.0. Personally I welcome these new features as I think it provides better functionality, and a cleaner solution too! For more information about C# 8.0, or different examples of pattern matching, check out this article in Microsoft Docs.
Thanks for reading and until the next blog post,
Bjorn


