So far in this series on C#7, we have looked at some nice new things, including out variables, new expression-bodied members, throw expressions, and binary numeric literals. These are all great little additions, but this week, we get to a truly cool and long-awaited feature; pattern matching. But, before we get into that, here is the usual summary of what I am covering.
- Binary literals
- Numeric literal digit separators
outvariablesthrowexpressions- Expression-bodied constructors
- Expression-bodied finalizers
- Expression-bodied property accessors
- Pattern matching
- Local functions
- Generalized async return types
reflocals and returns- Tuples
Pattern Matching
I will admit that I was a tad confused as to why this is called "pattern matching". I read the Wikipedia article1. I expect I would understand this more if I were a Computer Science major instead of Computer Systems Engineering2. According to Wikipedia:
In computer science, pattern matching is the act of checking a given sequence of tokens for the presence of the constituents of some pattern.
The What's New in C#7 documentation explains:
Pattern matching is a feature that allows you to implement method dispatch on properties other than the type of an object.
While I now understand the academic concept of pattern matching, I think we may regret using the term to specifically reference the features that are under its umbrella for C#7. I would have preferred it if these features had been introduced using already widely understood and consistent nomenclature. I think the official documentation agrees with me, since it almost immediately splits the feature into two pieces; is expressions and switch expressions. However, I am going to draw the line that divides the parts of pattern matching in a different place and call the two parts cast-conditional variables and case filters, because that fits better with my understanding of what they do.
Cast-conditional Variables
Many of us know the following code:
var x = y as MyType;
if (x != null)
{
//Do the awesome!
}
This is the efficient way of verifying a value is a given type and then consuming it as that type since we cast just once3. However, with the new cast-conditional feature, we can condense this down to:
if (y is MyType x)
{
//Do the awesome!
}
The cast and the check of its success have been condensed into a single, easily understood statement. We can use this cast-conditional variable syntax in any place where we might otherwise but a boolean expression:
// Example 1: Calculating a boolean bool isDoubleNan = value is double y && Double.IsNaN(y); // Example 2: Ternary operator string filePath = size is int x && x > 10 ? "C:\Archive" : "C:\TinyFiles";
This feels like a great improvement to me and fits really well with the similar out variable feature, however it is only a new way of writing something we could already do. When extended to the cases in switch statements (albeit with a slightly different syntax), this gives us a brand new ability; switching based on variable type:
switch (myInterface)
{
case MyTypeA a:
// Do something because we know this is of type, MyTypeA
break;
case MyTypeB b:
// Do something because we know this is of type, MyTypeB
break;
}
Now you can iterate over that array of object values and have a nice, succinct way of processing the contents by type. Not only that, but with case filters, you can craft even finer conditions for your switches.
Case Filters
C#6 introduced the when keyword as a modifier to catch expressions so that we could finally utilize exception filters from C#. Now, the when keyword gets a similar job in switch statements as a modifier to case statements. Like in exception filters, the when expression is a condition that must be met for the case to be a match. For example, we could create an IsNumber method and use when to filter cases like Infinity and NaN:
bool IsNumber(object value)
{
switch (value)
{
case int x:
case float y when !float.IsNaN(y) && !float.IsInfinity(y):
case double z when !double.IsNaN(z) && !double.IsInfinity(z):
return true;
default:
return false;
}
}
Prior to C#7, this code would look something like this:
bool IsNumberOld(object value)
{
int? x = value as int?;
if (x.HasValue) return true;
float? y = value as float?;
if (y.HasValue && !float.IsNaN(y.Value) && !float.IsInfinity(y.Value)) return true;
double? z = value as double?;
if (z.HasValue && !double.IsNaN(z.Value) && !double.IsInfinity(z.Value)) return true;
return false;
}
Not only was there more typing before C#7, but I think the code was more repetitive and harder to scan. This may be a convoluted example, but I hope it illustrates how valuable this new language syntax can be.
In Conclusion
Naming disagreements aside, the new pattern matching in C#7 is powerful. With that power comes responsibility; the responsibility to use it wisely and to call out others who do not, for there is surely great room for abuse with this feature. I envisage frequent and appropriate use of cast-conditional variables in if statements since the scenarios to which that caters are widespread. However, the filtering added to switch statements brings something entirely new and so, I do not see it being as widely adopted not as appropriately used; time will tell.
Overall, I love this addition to C#7 even though I do not like the name. What do you think? Does "pattern matching" make sense or should it be something like "cast-conditional variables" and "case filters" instead? Will you use this feature a lot? When might you find pattern matching useful? Sound off in the comments and discuss.
- that's a lie; I read parts of it until I came to the conclusion that it was not helping [↩]
- Is this a theory versus practice issue again? I face those often in this field [↩]
- Using the is condition and then casting inside the if statement would cause two casts; one for the is and another for the cast [↩]
