Use Pattern Matching in Switch Statements
Apr 29, 2019 • 8 Minute Read
Introduction
As explained in Using Conditional Statements Like If and Switch in C#, a switch statement can be used to execute different blocks of code based on the value of the control variable, also known as the match expression.
In C# 6.0 and earlier versions, the match expression could only be compared to a constant pattern - either a char, a string, a bool, a number (int or long), or an enum. Starting with C# 7.0, we can also use custom object types and their properties to define the flow of the program, a feature called pattern matching that we will discuss in this guide.
Note: You will need Visual Studio 2017 or newer to follow along. If you choose to use .NET Fiddle, be sure to use Roslyn as the compiler for your project. For your reference, the code for this guide is available in Github.
Introducing Pattern Matching
To get a grasp on the wonders of pattern matching, let us begin by considering the following Employee parent and Engineer and Developer children classes. Note that, for simplicity, we will assume the variable called name represents the employee's full name. In this example, language is the preferred programming language for a developer, and leader indicates whether the engineer currently leads a team or not.
class Employee {
private string name;
private int age;
public string Name {
get {
return this.name;
}
set {
this.name = value;
}
}
public int Age {
get {
return this.age;
}
set {
this.age = value;
}
}
}
class Engineer: Employee {
private bool leader;
public bool Leader {
get {
return this.leader;
}
set {
this.leader = value;
}
}
}
class Developer: Employee {
private string language;
public string Language {
get {
return this.language;
}
set {
this.language = value;
}
}
}
Next, we will create one employee, one developer, and two engineer objects:
Employee emp = new Employee
{
Name = "Mike Ehrmantraut",
Age = 45
};
Developer dev = new Developer
{
Name = "Walter White",
Age = 27,
Language = "JavaScript"
};
Engineer eng1 = new Engineer
{
Name = "Gus Fring",
Age = 32,
Leader = true
};
Engineer eng2 = new Engineer
{
Name = "Hector Salamanca",
Age = 49,
Leader = false
};
With that in mind, the following scenarios will help us illustrate the most important aspects of pattern matching.
Example 1: The Type Pattern
To begin, let us create a method called PrintPersonalInfo, which will take a generic person object as a parameter:
public static void PrintPersonalInfo(object person)
{
switch (person)
{
case Developer d:
Console.WriteLine($"{d.Name} is a {d.Age}-year-old developer with {d.Language} skills");
break;
case Engineer e:
Console.WriteLine($"{e.Name} is a {e.Age}-year-old engineer");
break;
}
}
In the code above, we see:
-
The match expression is compared against two custom objects (Developer and Engineer). More accurately, we are checking the type of the control variable, which is the same as saying that we are using a type pattern.
-
Variable instances d and e are assigned the value of person only when the pattern is matched (which means they are in scope only within their corresponding case blocks and cannot be used outside).
If we now call PrintPersonalInfo on each object, the output show be similar to Fig. 1:
PrintPersonalInfo(emp);
PrintPersonalInfo(dev);
PrintPersonalInfo(eng1);
PrintPersonalInfo(eng2);
You probably noticed there is no case block for the Employee object. This results in nothing being printed when the method is run as PrintPersonalInfo(emp), which leads us to what we will discuss in Example 2.
Example 2: Non-mutually Exclusive Values
To proceed, we will add the following case at the top of the switch block:
case Employee em:
Console.WriteLine($"{em.Name} is one of our employees");
break;
When we try to compile the project, we will get a CS8120 error (The switch case has already been handled by a previous case), as shown in Fig. 2:
The challenge here is that pattern matching in the switch structure opens the door for non-mutually exclusive values in separate case labels. Generally speaking, C# 7.0 and newer versions will complain if it detects a case that represents a subset of a previous statement. In this example, both Developer and Engineer are subclasses of Employee, which explains why we get the error twice in the debug window at the bottom of Fig. 2.
C# 6 and older versions only allowed mutually exclusive values through the constant pattern, so the order of the case statements was not important. That did change beginning with C# 7.0 as we can see above.
That being said, we can move the corresponding case block at the bottom of the switch section if we need to take into account the Employee object.
Example 3: Switching on Properties
For this example, let us replace the switch block in PrintPersonalInfo with the following code:
switch (person)
{
case Engineer e when e.Leader:
Console.WriteLine($"{e.Name} is currently leading a team");
break;
case Engineer e when !e.Leader:
Console.WriteLine($"{e.Name} is not leading a team these days");
break;
}
Fig. 3 shows the output of calling the method on the same objects as before. As expected, it only prints a message to the screen when executed as PrintPersonalInfo(eng1) and PrintPersonalInfo(eng2):
At first sight, this example does not add much to #1 - except for the use of the when keyword that now allows us to switch on the object's properties. Similarly, we can also check if an attribute matches a pattern with the following valid case labels:
case Engineer e when e.Name.Contains("Gus"):
or
case Developer d when d.Language.ToUpper().Contains("JAVASCRIPT"):
In example #2, we could also have used the when keyword as case Employee em when !(em is Developer || em is Engineer) to leave the associated block at the top of the switch section.
Of course, this also means that you can potentially use more complex regular expressions as conditions. Checking a string for a given substring is presented here as a representative illustration.
Summary
In this guide, we have examined the basics of pattern matching in switch statements. This feature gives us more flexibility by allowing us to use any non-null expression in match expressions. However, since with great power comes great responsibility, be sure to address non-mutually exclusive conditions. You can accomplish that goal by placing case blocks in the right order or using the when keyword as explained here.