📜 ⬆️ ⬇️

Pattern matching in C # 7

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 creating an application for which performance is critical. New levels of abstractions are good, but to use them effectively, 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:


Pattern matching in is expressions


public void IsExpressions(object o) { // Alternative way checking for null if (o is null) Console.WriteLine("o is null"); // Const pattern can refer to a constant value const double value = double.NaN; if (o is value) Console.WriteLine("o is value"); // Const pattern can use a string literal if (o is "o") Console.WriteLine("o is \"o\""); // Type pattern if (o is int n) Console.WriteLine(n); // Type pattern and compound expressions if (o is string 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:


First, consider the first two cases:

 public void ScopeAndDefiniteAssigning(object o) { if (o is string 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 is int n || (o is string 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:

 public void BoxTwice(int n) { if (n is 42) 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]:

 public void BoxTwice(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.

 public void IsVar(object o) { if (o is var 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 commentary by Neil 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:

 public void VarPattern(IEnumerable<string> s) { if (s.FirstOrDefault(o => o != null) is var v && int.TryParse(v, out var 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:

 public void WithNullPropagation(IEnumerable<string> s) { if (s?.FirstOrDefault(str => str.Length > 10)?.Length is int length) { Console.WriteLine(length); } // Similar to if (s?.FirstOrDefault(str => str.Length > 10)?.Length is var length2 && length2 != null) { Console.WriteLine(length2); } // And similar to var 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:

 public static int Count<T>(this IEnumerable<T> e) { switch (e) { case ICollection<T> c: return c.Count; case IReadOnlyCollection<T> c: return c.Count; // Matches concurrent collections case IProducerConsumerCollection<T> pc: return pc.Count; // Matches if e is not null case IEnumerable<T> _: return e.Count(); // Default case is handled when e is null default: return 0; } } 

This example shows the first set of changes to the switch statement.

  1. The switch statement can use any type of variable.
  2. The case clause can specify a pattern.
  3. 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.
  4. 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:

 public static void FizzBuzz(object o) { switch (o) { case string s when s.Contains("Fizz") || s.Contains("Buzz"): Console.WriteLine(s); break; case int n when n % 5 == 0 && n % 3 == 0: Console.WriteLine("FizzBuzz"); break; case int n when n % 5 == 0: Console.WriteLine("Fizz"); break; case int n when n % 3 == 0: Console.WriteLine("Buzz"); break; case int 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:

 public static void FizzBuzz(object o) { // All cases can match only if the value is not null if (o != null) { if (o is string s && (s.Contains("Fizz") || s.Contains("Buzz"))) { Console.WriteLine(s); return; } bool isInt = o is int; int num = isInt ? ((int)o) : 0; if (isInt) { // The type check and unboxing happens only once per group if (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 checking, 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. case int n when n == 1: return 1; case string s when s == "": return 2; case int n when n == 2: return 3; default: return -1; } 

The compiler converts it like this:

if (o is int n && n == 1) return 1;
 if (o is string s && s == "") return 2; if (o is int n2 && n2 == 2) return 3; return -1; 

2. The compiler does its best to avoid typical ordering problems.

 switch (o) { case int n: return 1; // Error: The switch case has already been handled by a previous case. case int n when n == 1: return 2; } 

However, the compiler cannot determine that one predicate is stronger than another, and effectively replaces the following case clauses:

 switch (o) { case int n when n > 0: return 1; // Will never match, but the compiler won't warn you about it case int n when n > 1: return 2; } 

Model Briefing



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

Source: https://habr.com/ru/post/423229/


All Articles