Изучение Сопоставления Шаблонов В C# 7

В C# 7 наконец-то появилась долгожданная функция, называемая сопоставлением с образцом ( сопоставление с образцом ).

Если вы знакомы с функциональными языками, такими как F#, возможно, вас немного разочарует эта функция в ее нынешнем виде, но даже сегодня она может упростить ваш код в самых разных сценариях.

Каждая новая функция сопряжена с риском для разработчика, работающего над приложением, критичным к производительности.

Новые уровни абстракции великолепны, но чтобы эффективно их использовать, вам нужно знать, что происходит «под капотом».

Сегодня мы рассмотрим внутреннюю структуру сопоставления с образцом, чтобы понять, как оно реализовано.

В языке C# введена концепция шаблона, который можно использовать в выражении is и внутри блока.

случай оператор выключатель .

Существует 3 типа шаблонов:

  • Константа шаблона
  • Введите шаблон
  • вар-шаблон


Сопоставление шаблонов в выражениях is

  
  
  
  
  
  
  
  
  
  
  
  
  
   

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"); }

Выражение is может проверить, равно ли значение константе, а проверка типа может дополнительно создать переменную шаблона ( переменная шаблона ).

Я обнаружил несколько интересных аспектов, связанных с сопоставлением шаблонов в выражениях is:

  • Переменная, введенная в оператор если , поднимается во внешнюю зону видимости.

  • Переменная, введенная в оператор если , полностью определен (определенно назначен) только тогда, когда шаблон соответствует.
  • Текущая реализация сопоставления константных шаблонов в выражениях is не очень эффективна.

Давайте сначала проверим первые два случая:

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); } }

Первый оператор если вводит переменную с , и переменная видна внутри всего метода.

Это разумно, но усложнит логику, если другие операторы if в том же блоке попытаются снова использовать то же имя.

В этом случае вы нужно используйте другое имя, чтобы избежать коллизий.

Переменная, введенная в выражение is, полностью определена только тогда, когда предикат истинный .

Это означает, что переменная н во втором заявлении если не определена в правом операнде, но поскольку эта переменная уже объявлена, мы можем использовать ее как переменную вне в методе int.TryParse .

Третий аспект, упомянутый выше, является наиболее важным.

Рассмотрим следующий код:

public void BoxTwice(int n) { if (n is 42) Console.WriteLine("n is 42"); }

В большинстве случаев выражение is преобразуется в object.Equals(constValue, переменная) (даже если в спецификации указано, что оператор == следует использовать для примитивных типов):

public void BoxTwice(int n) { if (object.Equals(42, n)) { Console.WriteLine("n is 42"); } }

Этот код вызывает 2 ящика (бокса), которые могут очень серьезно повлиять на производительность, если они используются на критическом пути приложения.

Когда-то выражение о равно нулю также вызвана упаковка (см.

Неоптимальный код для e равен нулю ) и надеюсь, что текущее поведение тоже скоро исправится (вот соответствующий билет на Гитхабе).

Если н -переменная имеет тип объект , Что о — 42 приведет к выделению одной памяти (для упаковки литерала 42 ), хотя такой код на основе переключателей не приводит к выделению памяти.



шаблон var в выражениях

Образец вар является особым случаем шаблона типа с одним ключевым отличием: шаблон будет соответствовать любому значению, даже если это значение нулевой .



public void IsVar(object o) { if (o is var x) Console.WriteLine($"x: {x}"); }

o объект верен , Когда о Нет нулевой , Но о - это вар х Всегда истинный .

Компилятор знает об этом и в режиме Release (*) полностью удаляет конструкцию if и просто оставляет вызов консольного метода.

К сожалению, компилятор не предупреждает о недостижимости кода в следующем случае: if (!(o is var x)) Console.WriteLine("Недоступно") .

Надеюсь, это тоже будет исправлено.

(*) Непонятно, почему поведение отличается только в режиме Release. Но я думаю, что все проблемы имеют одну и ту же природу: первоначальная реализация функции неоптимальна.

Но на основании этот комментарий Нил Гафт после этого изменится: «Код сопоставления с плохим шаблоном переписан с нуля (для поддержки рекурсивных шаблонов).

Я ожидаю, что большинство улучшений, которые вы ищете здесь, будут «бесплатными» в новом коде».

Отсутствие проверки на нулевой делает этот случай очень особенным и потенциально опасным.

Но если вы знаете, что именно происходит, эта опция сопоставления может оказаться вам полезной.

Его можно использовать для введения временной переменной в выражение:

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-выражение и оператор «Эlvis»

Есть еще один случай, который мне показался очень полезным.

Шаблон типа соответствует значению, только если это значение не равно нулевой .

Мы можем использовать эту логику «фильтра» с нулевое распространение оператор, чтобы сделать код более читабельным:

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); } }

Обратите внимание, что один и тот же шаблон можно использовать как для типов значений, так и для ссылочных типов.



Сопоставление шаблонов в блоках переключателей

C# 7 расширяет оператор выключатель для использования образцов в случай -блоки:

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; } }

В примере показан первый набор изменений в операторе переключения.

  1. В операторе выключатель можно использовать переменную любого типа.

  2. Предложение случай может указывать на закономерность.

  3. Порядок предложений важен случай .

    Компилятор выдает ошибку, если предыдущий случай соответствует базовому типу и следующему случай – соответствует производному типу.

  4. Все случай -блоки содержат неявную проверку на нулевой (**).

    В предыдущем примере последний случай -block верен, поскольку он сработает только тогда, когда аргумент не равен нулевой .

(**) Напоследок случай -block показывает еще одну функцию, добавленную в C# 7, которая называется «шаблон».

отказаться " Имя _ является особенным и сообщает компилятору, что переменная не нужна.

Введите образец в предложении случай требуется имя переменной, и если вы не собираетесь ее использовать, вы можете игнорировать ее с помощью _ .

В следующем фрагменте показана еще одна особенность сопоставления с образцом, основанная на выключатель — возможность использования предикатов:

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; } }

Выключатель может иметь более одного случай -блок того же типа.

В этом случае компилятор объединяет все проверки типов в один блок, чтобы избежать лишних вычислений:

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); } } }

Но нужно иметь в виду две вещи:

  1. Компилятор объединяет только последовательные случай -блоки одного типа, а если смешивать блоки разных типов, то компилятор будет генерировать неоптимальный код:

    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; }

    Компилятор преобразует его следующим образом:

    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. Компилятор делает все возможное, чтобы предотвратить распространенные проблемы нарушения порядка.

    случай -блоки

    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; }

    Но компилятор не знает, что один предикат сильнее другого, и, по сути, делает недоступными следующие блоки:

    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; }



Сопоставление с образцом 101

  • В C# 7 были представлены следующие шаблоны: шаблон const, шаблон типа, шаблон var и шаблон отказаться .

  • Шаблоны можно использовать в выражениях и в случай блоки
  • Реализация шаблона const в выражениях is для типов значений далека от совершенства с точки зрения производительности.

  • Шаблон var соответствует любому значению, и с ним следует быть осторожным.

  • Оператор выключатель может использоваться для установки проверок типов с дополнительными предикатами в предложениях когда .

Теги: #C++ #c# 7 #.

NET #сопоставление с образцом #.

NET #C++

Вместе с данным постом часто просматривают: