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
out
variablesthrow
expressions- Expression-bodied constructors
- Expression-bodied finalizers
- Expression-bodied property accessors
- Pattern matching
- Local functions
- Generalized async return types
ref
locals 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 [↩]