Time to cover yet another construct of the C# language. In this series, we’ve already covered the C# list, array, and string, to name a few. Today’s post is a little bit different, though. Its subject isn’t a data structure or type, but instead, a decision structure: the C# switch statement.
In this post you’ll learn all about the switch statement. What is it, how should you use it, and when should be careful using it?
Le’s get started!
C# Switch Statement: What Is It?
The switch is a selection (or decision) statement. It chooses a single section to execute, based on the value of a test variable. Consider the following example:
static void PrintPostInfo(Post post) { switch (post.Status) { case PostStatus.Draft: Console.WriteLine("The draft for the post {0} was last edited on {1}.", post.Title, post.Updated); break; case PostStatus.Published: Console.WriteLine("The post{0} was published on {1}. It has received {2} comments", post.Title, post.Published, post.CommentCount); break; case PostStatus.Scheduled: Console.WriteLine("The post {0} is scheduled for publication on {1}", post.Title, post.PublicationDate); break; case PostStatus.Deleted: Console.WriteLine("The post {0} was deleted on {1}", post.Title, post.DeleteDate); break; default: throw new InvalidEnumArgumentException(nameof(post), (int)post.Status, typeof(PostStatus)); } }
This is a simple method that prints a message to the console, based on the value of a property from the object received as a parameter. Here’s the equivalent code using if statements:
static void PrintPostInfo(Post post) { if (post.Status == PostStatus.Draft) { Console.WriteLine("The draft for the post {0} was last edited on {1}.", post.Title, post.Updated); } else if (post.Status == PostStatus.Published) { Console.WriteLine("The post{0} was published on {1}. It has received {2} comments", post.Title, post.Published, post.CommentCount); } else if (post.Status == PostStatus.Scheduled) { Console.WriteLine("The post {0} is scheduled for publication on {1}", post.Title, post.PublicationDate); } else if (post.Status == PostStatus.Deleted) { Console.WriteLine("The post {0} was deleted on {1}", post.Title, post.DeleteDate); } else { throw new InvalidEnumArgumentException(nameof(post), (int)post.Status, typeof(PostStatus)); } }
How to Use the Switch Statement
With the “what” out of the way, time to get to the “how.” Now we’re going to show how to actually use this structure in practice, starting out by breaking it down into its component parts.
The Anatomy of the C# Switch
The “switch”?statement begins?with the match expression, followed by n switch sections,?n being a number equals to or greater than 1. We’re now going to cover each of these parts in detail.
1. The Match Expression
The match expression is where you specify the value to be matched against the possible values. It follows this syntax:
switch (expression)
What exactly is this expression?
For versions through C# 6.0, it could be any expression that returns either a bool, a char, a string, or even an enum value (which is a very common use case). It could also be an integral value (for instance, a long or an int).
Starting at the 7th version of C# on, it’s possible to use any non-null expression as a match expression.?More on that later.
After that, what follows is one or more switch sections. A switch section, in its turn, contains one or more labels.
2. The Switch Section(s)
A switch statement contains at least one switch section.? A switch section, in turn, contains one or more case labels.
Each label is then followed by one or more statements. The C# language only allows a single switch section to be executed in any switch statement. For instance, the following code won’t compile:
static void PrintMessage(string message, MessageType type) { switch (type) { case MessageType.Info: PrintInfo(message); break; case MessageType.Error: PrintError(message); case MessageType.Question: PrintQuestion(message); break; } }
The error message we get is “Control cannot fall through from one case label (‘case MessageType.Error:’) to another.”
How do we fix this? Easy: let’s just add a break to the second switch section.
static void PrintMessage(string message, MessageType type) { switch (type) { case MessageType.Info: PrintInfo(message); break; case MessageType.Error: PrintError(message); break; case MessageType.Question: PrintQuestion(message); break; } }
I bet you probably already knew that. But here’s the thing. There’s nothing special about the break instruction.
You just have to make sure the control flow stops after the execution of the chosen section. You could use a return or throw an exception instead. For instance, the following version of the code above would work just as well:
static void PrintMessage(string message, MessageType type) { switch (type) { case MessageType.Info: PrintInfo(message); return; case MessageType.Error: PrintError(message); return; case MessageType.Question: PrintQuestion(message); return; } }
3. Case Labels
Each label provides a pattern for comparison. These patterns are then tested, in order, against the match expression.
When there is a match, the switch section that has the fist matching label receives control. If all case labels are verified but no matches are found, the default case section receives the control. If no section has the default case label, no statements are executed at all
The sixth version of the C# language only supports constants as case labels. Also, it doesn’t allow the repetition of values.
The implication of this is that, in C# 6, the case labels define values that are mutually exclusive. Because of that, the order of case statements doesn’t matter.
However, C# 7.0 changed things quite a bit.
Starting with this version, C# supports other types of patterns. Because of that, the values defined by the labels don’t need to be mutually exclusive, which means that the match expression can match more than one pattern.
You probably already know where this is going: the order of case statements now matters.
If you write a switch section containing one or more statements that are subsets of statements that appeared previously (or even equivalent to them), you’re going to get the CS8120 compiler error, with the following error message: “The switch case has already been handled by a previous case.”
The C# Switch Default Case
When you use the switch statement, what do you do when the expression value doesn’t match any of the?labels?
First, you must define what that means with respect to your business rules. It might be perfectly okay, or it might represent a mistake. In either case, C# provides the default case for handling those types of situations.
The “PrintMessage” method we’ve defined receives an enum as a parameter. Although an enum can have any integral type as its underlying type, the default is int.
Given an enum with int as its underlying type, you can cast any int value to the enum type and it will gladly compile. So as long as the compiler is concerned, the following line of code is perfectly valid:
PrintMessage("Why did the chicken cross the road?", (MessageType)123456123);
What happens when we run the code above? Well, nothing.
Since the number we’ve passed isn’t the value of a valid enumeration, the switch doesn’t match it with any switch section. The control flows until the end of the method, and nothing gets printed.
Would that be such a big deal?
Not in this very simple example we’re using, no. But it’s not that hard to imagine a serious, more complex application where each switch section calls a method that runs a critical operation. In that case, it would be an enormous deal to have the process fail to run without giving any warnings. That’s what we call “failing silently.”
In such a situation, it would be far more useful to fail fast by throwing an exception. The following code shows just how to do that:
switch (type) { case MessageType.Info: PrintInfo(message); break; case MessageType.Error: PrintError(message); break; case MessageType.Question: PrintQuestion(message); break; default: throw new InvalidEnumArgumentException(nameof(type), (int)type, typeof(MessageType)); }
In this iteration of our example, we’ve reverted the switch sections to using the break instruction instead of return. We’ve also added a new section under the “default” label. Now when someone passes it an integer that doesn’t match with the enumeration values, our method throws an exception of type “InvalidEnumArgumentException.”
Modern Switch: Welcome to Pattern Matching
Throughout this post, we?ve been saying time and time again that the C# switch statements now accept more patterns as case labels values. But what does that really mean, in practice? That?s what this section is going to cover.
First we?re going to briefly cover the constant pattern, a.k.a. the old, regular way. After that, we?ll talk about the type pattern, which is the novelty introduced by C# 7.0
The Switch Constant Pattern
The C# switch “constant pattern” is just a name for the old, regular way of doing things. It’s the way you’re used to, from C# 6.0.
It verifies whether the match expression matches the constant you’ve specified.
And what does ?constant? mean here? According to the docs by Microsoft, any of the following:
- A bool literal, either true or false.
- Any integral constant, such as int, long, or a byte.
- The name of a declared const variable.
- An enumeration constant.
- A char literal.
- A string literal.
The Switch Type Pattern
C# 7.0 introduced the ?type pattern.? This new pattern allows you to easily evaluate and convert between types.
Sure, you still could accomplish the same in older versions of C#, by employing several chained if-statements and casts. But with the switch statement new pattern-matching capabilities, you can do the same with way less code.
The syntax of the type pattern is as follows:
case type varname
According to the docs, those are the instances where the test evaluates to “true”:
- expr is an instance of the same type as type.
- expr is an instance of a type that derives from type. In other words, the result of expr can be upcast to an instance of type.
- expr has a compile-time type that is a base class of type, and expr has a runtime type that is type or is derived from type.
- The compile-time type of a variable is the variable’s type as defined in its type declaration. The runtime type of a variable is the type of the instance that is assigned to that variable.
- expr is an instance of a type that implements the type interface.
Let’s consider a quick code example:
private static void PrintUserInformation(object user) { switch (user) { case Admin admin: Console.WriteLine("The user is an admin."); break; case SuperUser superUser: Console.WriteLine("The user is a super user."); break; case IUser user: Console.WriteLine("The user is any other type of user."); break; case null: Console.WriteLine("User is null."); break; default: } }
Suppose our code features an IUser interface, which both the Admin and SuperUser classes implement.
The switch in the code sample above tests an instance of object against some type options. If ?user? is an instance of ?Admin?, then the first section is executed and ?The user is an admin.? is displayed. The same happens for ?SuperUser.?
If ?user? is neither ?Admin? nor ?SuperUser?, though, the instance is tested against the ?IUser? interface, and it?ll be evaluated as true if user is an instance of yet another class that implements the interface.
Now it becomes clear how the order matters when it comes to the type pattern.
If we had put the ?IUser? case label first, then it would match with instances of both ?Admin? and ?SuperUser?, generating wrong results. The general rule here is: order the labels from the most specific to the more general types.
Here Be Dragons: Why You Should Beware When Dealing With the C# Switch
Programming tools and techniques aren’t perfect. They can be used for good, but they’re also abused, and the switch statement is no different.
So what are the potential problems with the C# switch you should be aware of when writing code?
The main issue with this construct is that it’s often used to solve problems for which there are better-suited solutions. For example, people often use the switch statement to do a simple mapping:
static string GetInstrumentForMusician(string musician) { switch (musician) { case "Jimmy Page": return "guitar"; case "Keith Moon": return "drums"; case "Richard Wright": return "Keyboard"; case "Jack Bruce": return "bass guitar"; default: throw new ArgumentException("invalid musician"); } }
You could accomplish the same result by using a dictionary:
static string GetInstrumentForMusician(string musician) { var instrument = string.Empty; try { instrument = instruments[musician]; } catch (System.Collections.Generic.KeyNotFoundException) { throw new ArgumentException(musician + " is not a valid musician."); } return instrument; }
Which one is “better”?
That depends on so many factors, it doesn’t even make sense for me to try and give a general answer. As I like to say, write code with readability in mind. If performance becomes an issue, then benchmark the heck out of it and optimize where you need.
The biggest problem with switch statements, in general, is that they can be a code smell. Switch overuse might be a sign that you’re failing to properly employ polymorphism in your code.
If you see the same switch statement repeated across your application, it’s probably time to switch (no pun intended) your approach to the use of polymorphic structures.
Back to You Now
In this post you’ve learned a few things about the switch statement in C#: what is it, what problems it solves, how it’s used and, perhaps more importantly, how it can show you signs of potential problems within your application’s design.
Is this all there is to know about the C# switch? Of course not.
The C# language is always evolving, and things are changing fast these days. The language designers have been adding more capabilities to the switch statement in order to get to a feature called “pattern matching” which is commonly found in functional programming languages (such as F#).
This is just an example of things to come (and some things that are already here). So remember: to stay at the top of your game, never stop sharpening your saw.
You might also be interested in further reading about other C# language concepts:
Learn more how CodeIt.Right can help you automate code reviews and improve the quality of your code.
1 Comment. Leave new
In general it’s best to not use exception handling for control flow. Exceptions should be reserved for truly exceptional cases. In this case, using dictionary.TryGetValue would be more efficient, easier to maintain and avoid the icky exception based control flow.