Written 12/2019, Updated 7/2020
Most .NET engineers are familiar with the original switch
statement in C#. Like similar constructs in other object oriented languages, given an arbitrary expression, you can match its result to a case
, and execute selected statements.
switch (car.Color) {
case Color.Red:
Console.WriteLine("Color is red!");
break;
case Color.Blue:
Console.WriteLine("Color is blue!");
break;
default:
Console.WriteLine("Color is not red or blue!");
break;
}
In the past, I’ve found that switch
statements were useful for cleaning up long if else
chains, but I rarely found myself using them in code. To me, the switch-case-break
syntax feels bloated with keywords, and, before C# 7, cases only supported the constant pattern. This meant that each case value had to be a compile-time constant.
Fast forward to C# 8, and the lowly switch
statement has been upgraded with new features that make it much more appealing! Take a look at how we can simplify the above example:
Console.WriteLine(car.Color switch
{
Color.Red => "Color is red!",
Color.Blue => "Color is blue!",
_ => "Color is not red or blue!"
});
In this article, we’ll cover five new matching patterns and three other switch
improvements that can make complex control flow short and readable.
Any features in this section will be compatible with .NET Core >=2.0, .NET Standard >=1.0, and all versions of .NET Framework. See the C# language versioning article for more information.
The declaration pattern was introduced in C# 7. It enables case
matching based on the type of value passed in. The syntax is as follows:
var person = new { Name = "Drake" };
switch (person.Name)
{
// type followed by designation
// variable, name, will always be non-null if matched
// only matches values assignable from the given type
case string name:
Console.WriteLine($"Hello, {name}!");
break;
// only matches null values
case null:
Console.WriteLine($"Hello, user!");
break;
}
At first glance, its uses appear narrow, but when combined with polymorphism, the pattern is much more powerful. Consider this simple inheritance structure that models shapes:
class Square : Shape
{
public double Height { get; set; }
public double Width { get; set; }
}
class Circle : Shape
{
public double Radius { get; set; }
}
We can define an extension method for Shape
that calculates area depending on the type of shape passed in.
static double GetArea(this Shape shape)
{
switch (shape)
{
// match Rectangle type
case Rectangle rectangle:
// rectangle variable is:
// - non-null and of type Rectangle
// - in scope within the case
return rectangle.Height * rectangle.Width;
case Circle circle:
return Math.PI * circle.Radius * circle.Radius;
case null:
throw new ArgumentNullException(nameof(shape));
default:
throw new NotImplementedException();
}
}
Expressions always match the var pattern. In this way, it’s a suitable replacement for the default
case
when you have the additional requirement to capture the result of the expression being ‘switched’ on. The var pattern matches null
values. Consider the following example:
static bool TestShapeRequirement(this Shape shape)
{
switch (shape.GetArea())
{
// the constant pattern
case 0:
Console.WriteLine("A shape with non-zero area is required!");
return false;
// catch null case first
case null:
Console.WriteLine("A non-null area is required!");
return false;
// var followed by designation
case var area:
Console.WriteLine($"Shape with area {area} accepted!");
return true;
}
}
Case guards give additional control over the execution of case blocks with the when
clause.
static double GetAreaOptimized(this Shape shape)
{
switch (shape)
{
// 'when' followed by boolean expression
case Rectangle rectangle when rectangle.Height is 0 || rectangle.Width is 0:
case Circle circle when circle.Radius is 0:
return 0;
// ...
}
}
Any boolean valued expression can follow the when
keyword. This gives the developer the ability to conditionally execute each case
based on an expression evaluated at runtime rather than a compile time constant or other pattern.
By the way, if you haven’t seen the is
keyword before, you’ll love it! It takes all the pattern matching goodness seen in this article and allows us to use it in any context as a boolean valued expression. See C# 7 Pattern Matching - Is Expression for more information.
The following features are only available in C# 8 and above. Without changing compiler settings, you’ll only be able to use these in .NET Core >=3.0 and .NET Standard >=2.1.
Introduced in C# 8, the switch
expression addresses my primary gripe with the switch
statement, the syntax. switch
expressions remove the need for the case
, break
, and default
keywords, but they also go one step further by turning the switch
statement into an expression!
This allows us to convert the GetArea
method into this much more readable version:
static double GetAreaExpression(this Shape shape)
{
return shape switch
{
Rectangle rectangle => rectangle.Height * rectangle.Width,
Circle circle => Math.PI * circle.Radius * circle.Radius,
null => throw new ArgumentNullException(nameof(shape)),
var unknownShape => throw new NotImplementedException(),
};
}
First, take note that the expression or variable to be switched on now proceeds the switch
keyword, and parentheses are no longer required. Each case takes the form of (pattern) (optional when clause) => (expression)
where the expression to the right of the =>
is returned by the switch expression if the pattern matches. Each case
is ended with a comma.
Each switch
expression case
can also incorporate the when
clause:
shape switch {
Circle circle when circle.Radius is 0 => 0,
// ...
}
If you combine the switch
expression with C# 6 and 7 expression bodied members, you can really reduce the amount of boilerplate code you have to write:
static double GetAreaExpression(this Shape shape) => shape switch
{
Rectangle rectangle => rectangle.Height * rectangle.Width,
Circle circle => Math.PI * circle.Radius * circle.Radius,
null => throw new ArgumentNullException(nameof(shape)),
var unknownShape => throw new NotImplementedException()
};
The discard pattern in C# 8 is simple, and fills a similar role as the default
keyword.
shape switch
{
Rectangle rectangle => rectangle.Height * rectangle.Width,
Circle circle => Math.PI * circle.Radius * circle.Radius,
_ => throw new ArgumentException()
};
Here, the _
token matches anything. Unlike the declaration or var pattern, you cannot access the matched value via the _
token. Use this pattern to replace a default
case.
You can use the discard pattern in conjunction with the declaration pattern when you are only concerned with type matching, but not the captured value.
shape switch
{
Rectangle _ => "It's a rectangle!",
Circle _ => "It's a circle!",
_ => "I don't know what it is!"
};
This pattern is also useful in combination with other patterns seen later in this article.
The positional pattern has a tuple-like syntax. It allows pattern matching on a any type with a Deconstruct
method, but it’s most easily used with tuples.
The following example shows the ease with which you can write a complex state machine using the positional pattern.
var nextState = (GetShipmentState(), action, GetDaysSinceDelivery()) switch
{
(State.Ordered, Action.CancelOrder, _) => State.Canceled,
(State.Canceled, Action.CancelOrderCancellation, _) => State.Ordered,
(State.Delivered, Action.Return, int elapsedDays) when elapsedDays <= 30 => State.Returned,
(null, Action.Order, _) => State.Ordered,
(var state, _, _) => state
};
Note here that the input to the switch expression is an inline tuple, and each case deconstructs the tuple while performing additional pattern matching on each element.
Property patterns are very similar to positional patterns. They differ by pattern matching on named properties and fields of values rather than positional matching on tuple elements.
static double GetAreaExpression(this Shape shape) => shape switch
{
Rectangle { Width: 0 } => 0,
Rectangle { Height: 0 } => 0,
Circle { Radius: 0 } circle => circle.Radius,
Rectangle rectangle => rectangle.Height * rectangle.Width,
Circle circle => Math.PI * circle.Radius * circle.Radius,
_ => throw new ArgumentException()
};
In this pattern, a type name is followed by { ... }
where additional pattern matching can be performed on properties of the matched type inside the { }
. The result of a matched type can also be captured by a variable designation following the { }
. Matching this pattern ensures the value is not null
.
If you want to use the positional pattern directly on the switch
input type, you can omit the type before the { }
.
var person = new { Name = "Drake", Age = 22 };
var hasAccess = person switch
{
{ Name: "Drake" } drake when drake.Age > 18 => true,
{ Name: "Devin" } => true,
_ => false
};
All of the patterns we have seen so far can be combined in a recursive way! Given an arbitrarily complex object, we can combine property patterns, positional patterns, and the rest to select very specific criteria on values in a switch expression.
var message = complexType switch
{
{ ShipmentStatus: Shipment.State.Ordered } => "Congrats on your order!",
{ Address: { State: "LA" } } => "I live there too!",
{ Address: { Zip: null } } => "You forgot to enter a zip code!",
{ ShipmentStatus: Shipment.State.Delivered, Name: (var firstName, _) } => $"Enjoy your package {firstName}!",
null => throw new ArgumentNullException(),
_ => "I'm not sure what I'm looking at here."
};
Pattern matching with switch expressions gives C# developers a concise yet powerful way to express complex control flow. I find this is very helpful when writing functional C#, and for easily codifying complex business rules.
Feel free to reach out to me with any questions. I would also greatly appreciate your feedback on this article as I seek to improve it!
If you find this post useful, and wish to support it, you can below!