Part 0 - Nullabe Reference TypesPart 1 - Default Interface MembersPart 2 - Pattern Matching Enhancements
Part 3 - All the Small Things Next in the series we will pay attention to the enhancements to pattern matching. The biggest feature on this front is not the new patterns but the switch expression. I have previously expressed my dislike for the switch statement and luckily some of the issues are fixed with the new expression version.
So what is this all about? There is a new syntax for switch which as expressions do produces a value (or an exception). This is what it looks like:
DayOfWeek day = DayOfWeek.Monday;
bool isWeekday = day switch
{
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => throw new InvalidOperationException($"Unexpected day of the week {day}")
};
The switch expression has a value (it is an expression after all) and you can use that value. The bulky syntax is replaced by =>, no cases, no breaks. The switch expression is also exhaustive meaning it checks if all values are covered. Currently this means that the default clause is mandatory (after all you are not going to write all the possible int values). If you skip the default clause (designated with _) you get a warning. Sadly the designers have chosen to display the warning even if you cover all the values of the enum because an enum is just an int and you can have a value that is not part of the enum definition. In my opinion this is a mistake, but it can be fixed in the future. There are suggestions about different warnings that can be silenced and also a proposal for something called closed enums which are not just an int and can’t hold values which are not listed. Despite
numerous complaints it seems like this obviously suboptimal behavior will stay for now. Note that if you run the code with a warning the compiler still adds the default clause and a proper exception which makes the warning even more useless.
Even this way the switch expression is great and improves not only pattern matching but a bunch of code that would use switch today. The new syntax is shorter and clearer and the ability to assign the result to something instead of assign to a variable in each case simplifies switch-style code quite a bit. The warning on a missing default clause is useful too in cases where we switch on something other than enum.
Patterns
As I noted in
my comments on C# 7.0 pattern matching is a feature that benefits from adding more sugar to it. It is hard to have too many patterns. After all the goal is to decompose some data automatically and the more ways we have to do
this the more cases we can handle declaratively. So here are the new patterns which of course can be combined with the existing patterns
Property patterns allow us to match on a property value. For example
public static decimal ComputeSalesTax(Address location, decimal salePrice)
{
return location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.075M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};
}
This pattern checks the State property and returns the specified expression if the value matches.
The
tuple pattern allows us to check the values in a tuple like this
public static string RockPaperScissors(string first, string second)
{
return (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie"
};
}
Similar but distinct pattern is the
positional pattern which can be used if an object has a Deconstruct method. This method is used to decompose the object and apply additional patterns.
static Quadrant GetQuadrant(Point point)
{
point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};
}
There are also
recursive patterns which allow applying patterns to the result of other patterns. Here is an example I modified from the
advanced pattern matching tutorialpublic decimal CalculateToll(object vehicle)
{
return vehicle switch
{
Car { Passengers: 0} => 2.00m + 0.50m, // Type pattern + property pattern
Car { Passengers: 1} => 2.0m,
Car { Passengers: 2} => 2.0m - 0.50m,
Car c => 2.00m - 1.0m,
Taxi t => t.Fares switch // recursive pattern
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), // this is a property pattern that matches non-null objects
null => throw new ArgumentNullException(nameof(vehicle))
};
}
We can see how combining the patterns increases the expressiveness significantly. Sadly, we’re still not at the point where the real power of pattern matching in C# will shine. I expect that this will happen with C# 9.0 when relational (greater than >, less than <), conjunctive (and), disjunctive (or) and negated (not) patterns are added to the language. Then we will be able to cover the full range of an int with relational operators and the compiler will be able to prove if we missed an interval. Combined with the existing patterns it will be possible to express a lot of business logic with patterns.