Pattern matching in C # 7
- Transfer
In C # 7, the long-awaited feature called pattern matching finally appeared. If you are familiar with functional languages, such as F #, then this function as it exists at the moment may disappoint you a little. But even today, it can simplify the code in a variety of cases. More under the cut!
Each new feature can be dangerous for a developer building an application for which performance is critical. New levels of abstractions are good, but to effectively use them, you need to understand how they actually work. This article discusses the pattern matching function and how it works.
The pattern in C # can be used in the is expression, as well as in the case block of the switch statement.
There are three types of samples:
Using the is expression, you can check whether the value is constant, and using the type check, you can additionally determine the sample variable.
When using pattern matching in is expressions, you should pay attention to several interesting points:
First, consider the first two cases:
The first if statement introduces the variable s, visible inside the entire method. This is reasonable, but it will complicate the logic if other if expressions in the same block try to reuse the same name. In this case, be sure to use a different name to avoid conflicts.
The variable entered in the is expression is explicitly assigned only when the predicate is true. This means that the variable n in the second if expression is not assigned in the right-hand operand, but since it is already declared, we can use it as the out variable in the int.TryParse method.
The third point mentioned above is the most important. Consider the following example:
In most cases, the expression is converted to object.Equals (constant, variable) [although the characteristics state that for simple types you should use the == operator]:
This code triggers two packaging-transformation processes that can significantly affect performance if used on the critical path of the application. Previously, the expression o is null caused packing if the variable o had a type that is null-capable (see Suboptimal code for e is null (“Non-optimal code for e is null”)), but it is hoped that this will be corrected (here is the corresponding request on github ).
If the variable n is of type object, then the expression o is 42 will invoke one packing-transform process (for literal 42), although a similar code based on the switch statement would not lead to this.
A sample variable is a special kind of sample type with one big difference: the sample will match any value, even null.
The expression o is object will be true if o is not null, but the expression o is var x will always be true. Therefore, the compiler in release mode * completely eliminates if statements and simply leaves the call to the Console method. Unfortunately, the compiler does not warn about the inaccessibility of the code in the following case: if (! (O is var x)) Console.WriteLine ("Unreachable"). There is hope that this too will be fixed.
* It is not clear why behavior differs only in release mode. It seems that the root of all the problems is the same: the initial implementation of the function is not optimal. However, judging by this commentNeal Gafter, everything will change soon: “The code for pattern matching will be rewritten from scratch (to also support recursive patterns). I think that most of the improvements that you are talking about will be implemented in the new code and are available for free. However, this will take some time. ”
The absence of a null check makes this situation special and potentially dangerous. However, if you know the exact principles of this sample, then it may be useful. It can be used to insert a temporary variable into the expression:
There is another case that may be useful. A sample type corresponds to a value only when it is not null. We can use this “filtering” logic with a null propagation operator to make the code more readable:
Note that the same pattern can be used for both value types and reference types.
In C # 7, the functionality of the switch operator has been extended, so that samples can now be used in case clauses:
This example shows the first set of changes to the switch statement.
** In the last case clause, another function added to C # 7 is an empty variable pattern. The special name _ tells the compiler that the variable is not needed. The type pattern in the case clause requires a pseudonym. But if you don't need it, you can use _.
The following fragment shows another feature of pattern matching based on the switch operator — the ability to use predicates:
This is a strange version of the FizzBuzz task , in which the object is processed, and not just a number.
A switch statement can include multiple case clauses with the same type. In this case, the compiler combines all type checks to avoid unnecessary computation:
But you need to remember two things:
1. The compiler combines only sequential type checks, and if you mix case clauses with different types, less quality code will be generated:
The compiler converts it as follows:
if (o is int n && n == 1) return 1;
2. The compiler does its best to avoid typical ordering problems.
However, the compiler cannot determine that one predicate is stronger than another, and effectively replaces the following case clauses:
October 11, Thursday, Unity Moscow Meetup 2018.1 will be held at VSHBI. This is the first meeting of Unity developers in Moscow this season. The theme of the first mitap will be AR / VR. You are waiting for interesting reports, communication with industry professionals, as well as a special demo zone from MSI.
Details
Each new feature can be dangerous for a developer building an application for which performance is critical. New levels of abstractions are good, but to effectively use them, you need to understand how they actually work. This article discusses the pattern matching function and how it works.
The pattern in C # can be used in the is expression, as well as in the case block of the switch statement.
There are three types of samples:
- sample constants;
- sample type;
- sample variable.
Pattern matching in is expressions
publicvoidIsExpressions(object o)
{
// Alternative way checking for nullif (o isnull) Console.WriteLine("o is null");
// Const pattern can refer to a constant valueconstdoublevalue = double.NaN;
if (o isvalue) Console.WriteLine("o is value");
// Const pattern can use a string literalif (o is"o") Console.WriteLine("o is \"o\"");
// Type patternif (o isint n) Console.WriteLine(n);
// Type pattern and compound expressionsif (o isstring s && s.Trim() != string.Empty)
Console.WriteLine("o is not blank");
}
Using the is expression, you can check whether the value is constant, and using the type check, you can additionally determine the sample variable.
When using pattern matching in is expressions, you should pay attention to several interesting points:
- The variable entered by the if statement is sent to the outer scope.
- The variable entered by the if statement is explicitly assigned only when the sample falls.
- The current implementation of pattern matching of a constant in is expressions is not very efficient.
First, consider the first two cases:
publicvoidScopeAndDefiniteAssigning(object o)
{
if (o isstring s && s.Length != 0)
{
Console.WriteLine("o is not empty string");
}
// Can't use 's' any more. 's' is already declared in the current scope.if (o isint n || (o isstring s2 && int.TryParse(s2, out n)))
{
Console.WriteLine(n);
}
}
The first if statement introduces the variable s, visible inside the entire method. This is reasonable, but it will complicate the logic if other if expressions in the same block try to reuse the same name. In this case, be sure to use a different name to avoid conflicts.
The variable entered in the is expression is explicitly assigned only when the predicate is true. This means that the variable n in the second if expression is not assigned in the right-hand operand, but since it is already declared, we can use it as the out variable in the int.TryParse method.
The third point mentioned above is the most important. Consider the following example:
publicvoidBoxTwice(int n)
{
if (n is42) Console.WriteLine("n is 42");
}
In most cases, the expression is converted to object.Equals (constant, variable) [although the characteristics state that for simple types you should use the == operator]:
publicvoidBoxTwice(int n)
{
if (object.Equals(42, n))
{
Console.WriteLine("n is 42");
}
}
This code triggers two packaging-transformation processes that can significantly affect performance if used on the critical path of the application. Previously, the expression o is null caused packing if the variable o had a type that is null-capable (see Suboptimal code for e is null (“Non-optimal code for e is null”)), but it is hoped that this will be corrected (here is the corresponding request on github ).
If the variable n is of type object, then the expression o is 42 will invoke one packing-transform process (for literal 42), although a similar code based on the switch statement would not lead to this.
Variable pattern in is expression
A sample variable is a special kind of sample type with one big difference: the sample will match any value, even null.
publicvoidIsVar(object o)
{
if (o isvar x) Console.WriteLine($"x: {x}");
}
The expression o is object will be true if o is not null, but the expression o is var x will always be true. Therefore, the compiler in release mode * completely eliminates if statements and simply leaves the call to the Console method. Unfortunately, the compiler does not warn about the inaccessibility of the code in the following case: if (! (O is var x)) Console.WriteLine ("Unreachable"). There is hope that this too will be fixed.
* It is not clear why behavior differs only in release mode. It seems that the root of all the problems is the same: the initial implementation of the function is not optimal. However, judging by this commentNeal Gafter, everything will change soon: “The code for pattern matching will be rewritten from scratch (to also support recursive patterns). I think that most of the improvements that you are talking about will be implemented in the new code and are available for free. However, this will take some time. ”
The absence of a null check makes this situation special and potentially dangerous. However, if you know the exact principles of this sample, then it may be useful. It can be used to insert a temporary variable into the expression:
publicvoidVarPattern(IEnumerable<string> s)
{
if (s.FirstOrDefault(o => o != null) isvar v
&& int.TryParse(v, outvar n))
{
Console.WriteLine(n);
}
}
Is expression and operator Elvis
There is another case that may be useful. A sample type corresponds to a value only when it is not null. We can use this “filtering” logic with a null propagation operator to make the code more readable:
publicvoidWithNullPropagation(IEnumerable<string> s)
{
if (s?.FirstOrDefault(str => str.Length > 10)?.Length isint length)
{
Console.WriteLine(length);
}
// Similar toif (s?.FirstOrDefault(str => str.Length > 10)?.Length isvar length2 && length2 != null)
{
Console.WriteLine(length2);
}
// And similar tovar length3 = s?.FirstOrDefault(str => str.Length > 10)?.Length;
if (length3 != null)
{
Console.WriteLine(length3);
}
}
Note that the same pattern can be used for both value types and reference types.
Pattern matching in case blocks
In C # 7, the functionality of the switch operator has been extended, so that samples can now be used in case clauses:
publicstaticint Count<T>(this IEnumerable<T> e)
{
switch (e)
{
case ICollection<T> c: return c.Count;
case IReadOnlyCollection<T> c: return c.Count;
// Matches concurrent collectionscase IProducerConsumerCollection<T> pc: return pc.Count;
// Matches if e is not nullcase IEnumerable<T> _: return e.Count();
// Default case is handled when e is nulldefault: return0;
}
}
This example shows the first set of changes to the switch statement.
- The switch statement can use any type of variable.
- The case clause can specify a pattern.
- The order of sentences is important. The compiler will give an error if the previous sentence matches the base type, and the subsequent one is derived.
- Non-standard sentences are implicitly checked for null **. In the example above, the last case clause is valid, since it matches only when the argument is not null.
** In the last case clause, another function added to C # 7 is an empty variable pattern. The special name _ tells the compiler that the variable is not needed. The type pattern in the case clause requires a pseudonym. But if you don't need it, you can use _.
The following fragment shows another feature of pattern matching based on the switch operator — the ability to use predicates:
publicstaticvoidFizzBuzz(object o)
{
switch (o)
{
casestring s when s.Contains("Fizz") || s.Contains("Buzz"):
Console.WriteLine(s);
break;
caseint n when n % 5 == 0 && n % 3 == 0:
Console.WriteLine("FizzBuzz");
break;
caseint n when n % 5 == 0:
Console.WriteLine("Fizz");
break;
caseint n when n % 3 == 0:
Console.WriteLine("Buzz");
break;
caseint n:
Console.WriteLine(n);
break;
}
}
This is a strange version of the FizzBuzz task , in which the object is processed, and not just a number.
A switch statement can include multiple case clauses with the same type. In this case, the compiler combines all type checks to avoid unnecessary computation:
publicstaticvoidFizzBuzz(object o)
{
// All cases can match only if the value is not nullif (o != null)
{
if (o isstring s &&
(s.Contains("Fizz") || s.Contains("Buzz")))
{
Console.WriteLine(s);
return;
}
bool isInt = o isint;
int num = isInt ? ((int)o) : 0;
if (isInt)
{
// The type check and unboxing happens only once per groupif (num % 5 == 0 && num % 3 == 0)
{
Console.WriteLine("FizzBuzz");
return;
}
if (num % 5 == 0)
{
Console.WriteLine("Fizz");
return;
}
if (num % 3 == 0)
{
Console.WriteLine("Buzz");
return;
}
Console.WriteLine(num);
}
}
}
But you need to remember two things:
1. The compiler combines only sequential type checks, and if you mix case clauses with different types, less quality code will be generated:
switch (o)
{
// The generated code is less optimal:// If o is int, then more than one type check and unboxing operation// may happen.caseint n when n == 1: return1;
casestring s when s == "": return2;
caseint n when n == 2: return3;
default: return-1;
}
The compiler converts it as follows:
if (o is int n && n == 1) return 1;
if (o isstring s && s == "") return2;
if (o isint n2 && n2 == 2) return3;
return-1;
2. The compiler does its best to avoid typical ordering problems.
switch (o)
{
caseint n: return1;
// Error: The switch case has already been handled by a previous case.caseint n when n == 1: return2;
}
However, the compiler cannot determine that one predicate is stronger than another, and effectively replaces the following case clauses:
switch (o)
{
caseint n when n > 0: return1;
// Will never match, but the compiler won't warn you about itcaseint n when n > 1: return2;
}
Model Briefing
- The following patterns appeared in C # 7: a constant pattern, a pattern type, a sample variable, and a sample empty variable.
- Patterns can be used in is expressions and in case blocks.
- The implementation of the sample constant in the is expression for value types is far from ideal in terms of performance.
- Variable patterns are always the same, you have to be careful with them.
- The switch statement can be used for a set of type checks with additional predicates in when clauses.
Unity event in Moscow - Unity Moscow Meetup 2018.1
October 11, Thursday, Unity Moscow Meetup 2018.1 will be held at VSHBI. This is the first meeting of Unity developers in Moscow this season. The theme of the first mitap will be AR / VR. You are waiting for interesting reports, communication with industry professionals, as well as a special demo zone from MSI.
Details