The notion of patterns was introduced in C# version 7.0 and has taken ideas from functional programming to simplify and reduce your code. The concept is used in situations where we need to test that a value has a certain shape. Also, we can extract information from that value when it does.
Let’s start with a simple example.
var customer = GetCustomer(1);
if(customer is Customer)
{
WriteLine(customer.Name);
}
You will notice that this example of the is operator is hardly new. We have had this from the beginning of the language. The is operator, in this case, is used to check whether the run-time type of an object is compatible with a given type. C# 7 adds functionality to the operator.
if(customer is null)
{
WriteLine("There is no customer with this customer id.");
}
Using it this way we check the value customer against the constant pattern of null. Which is perhaps a bit silly, because we could simply replace it with == and have the same result. The same applies to other constants, such as string constants. If we were to declare something like this.
const string COMPANY = "Acme INC";
We can then test for it, like so.
if(customer is Customer && customer.Company is COMPANY )
The end result would be the same as when we to test with ==.
The real value of pattern matching comes when using it in evaluating objects retrieved from resources that you don’t completely control. Consider the following code.
object[] list = { "Charles", GetCustomer(1), GetCustomer(2), 1 };
foreach(var item in list)
{
if(item is Customer customer)
{
WriteLine(customer.Name);
}
if(item is string simpleName)
{
WriteLine(simpleName);
}
if(item is int i)
{
WriteLine(i);
}
}
Perhaps you noticed that the difference to the syntax available before C# 7.0 is that if the pattern matches, the object is assigned to a new variable of the specified type. This is very useful, as you get strongly-typed access to the members of the type, e.g. with the Customer type.
if(item is string simpleName)
{
WriteLine(simpleName);
}
Also, you can use logical operators to evaluate even further.
if(item is Customer customer && customer.CustomerId is 1 && customer.Name.Length > 0)
{
WriteLine(customer.Name);
}
As you can see, the pattern variables, that is the variables introduced by a pattern, are similar to out variables. They can be declared in the middle of an expression, and can be used within the nearest surrounding scope. Also like out variables, pattern variables are mutable.
Instead of specifying the type, you can also use the var keyword. Because an object is always of a type, this evaluation always succeeds.
if(item is var something)
{
WriteLine(something?.GetType().Name ?? "That's nothing");
}
As a personal observation, using var this way sort of defeats the purpose of new pattern matching feature in C# 7. Also note that we are using the null conditional operator which was introduced in C# 6.
You can use pattern matching in the switch statement as well. Evaluating a case with the const pattern was possible before, but there’s also a type pattern in which you declare a variable of the type, and the var pattern. The syntax is this.
case type varname
The case expression is true if any of the following is true:
- The value is an instance of the same type as type.
- The value is an instance of a type that derives from type. In other words, the value can be upcast to an instance of type.
- The value has a compile-time type that is a base class of type, and 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.
- The value is an instance of a type that implements the type interface.
If the case expression is true, a variable is definitely assigned and has local scope within the switch section only. Here’s an example.
switch (item)
{
case 1:
WriteLine("Item is 1");
break;
case Customer customer when customer.Name.Length > 0:
WriteLine(customer.Name);
break;
case Customer customer:
WriteLine(customer.CustomerId);
break;
default:
break;
case null:
WriteLine("No object.");
break;
}
Note that we are using the when keyword to enhance the pattern filter, just like we do in exception filters since C# 6. This also means that the order of the case clauses matters now. The first case that matches is the he one picked. It is therefore important to have specific clauses above more generic clauses, just like dealing with exception handling. The compiler will help you, by flagging obvious cases that can never be reached. If we were to switch the two case clauses that test for Customer, we get the following error.
error CS8120: The switch case has already been handled by a previous case
There is an additional change as a result of the new and improved switch-statement. The default clause is always evaluated last. This is for compatibility with existing switch semantics. However, good practice would usually have you put the default clause at the end. The example above is just to illustrate that the null clause at the end is not unreachable.
Conclusion
Pattern matching is a great feature that borrows its ideas from functional programming and helps you simplify and reduce your code. Patterns are a new kind of language element in C#, and we expect to have more of these, like positional patterns, property patterns, and recursive patterns in future versions of C#.
More resources
Leave a Comment