Шрифт
Исследуем сопоставление с образцом в C# 7
Play

Исследуем сопоставление с образцом в C# 7

В C# 7 наконец появилась долгожданная возможность под названием «сопоставление с образцом» (pattern matching). Если вы знакомы с функциональными языками, такими как F#, вы можете быть немного разочарованы этой возможностью в ее текущем виде, но даже сегодня она может упростить ваш код в самых разных сценариях.

Каждая новая возможность чревата опасностью для разработчика, работающего в критическом для производительности приложении. Новые уровни абстракций хороши, но для того, чтобы эффективно использовать их, вы должны знать, что происходит под капотом. Сегодня мы собираемся изучить внутренности сопоставления с образцом, чтобы понять, как это реализовано. Язык C# ввел понятие образца, которое может использоваться в is-выражении и внутри блока case оператора switch.

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

  • Шаблон const
  • Шаблон типа
  • Шаблон var
Сопоставление с образцом в is-выражениях

is-выражение может проверить, равно ли значение константе, а проверка типа может дополнительно создавать переменную образца (pattern variable).

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

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

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

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

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

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

Этот код вызывает 2 упаковки (boxing), которые могут весьма серьезно повлиять на производительность, если они используются в критическом пути приложения. Когда-то выражение o is null так же вызывало упаковку (см. Suboptimal code for e is null) и, я надеюсь, что текущее поведение так же будет исправлено в скором времени (вот соответствующий тикет на гитхабе).

Если n-переменная имеет тип object, то o is 42 приведет к одному выделению памяти (для упаковки литерала 42), хотя подобный код на основе switch не приводит к выделениям памяти.

var pattern в is- выражениях

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

o is object истинно, когда o не null, но o is var x всегда истинно. Компилятор знает об этом и в режиме Release (*) полностью удаляет конструкцию if и просто оставляет вызов консольного метода. К сожалению, компилятор не предупреждает, что код недостижим в следующем случае: if (!(o is var x)) Console.WriteLine(«Unreachable»). Надеюсь, это тоже будет исправлено.

(*) Непонятно, почему поведение отличается только в режиме Release. Но я думаю, что все проблемы имеют одну природу: первоначальная реализация фичи неоптимальна. Но на основе этого комментария Нила Gafter это изменится: «Плохой код, соответствующий сопоставлению с образцом, переписывается с нуля (для поддержки рекурсивных шаблонов тоже). Я ожидаю, что большинство улучшений, которые вы ищете здесь, будут „бесплатными“ в новом коде.».

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

Is-expression и «Элвис»-оператор

Есть другой случай, который я нашел очень полезным. Образец типа соответствует значению, только если значение не равно null. Мы можем использовать эту «фильтрующую» логику с null-propagating оператором, чтобы сделать код более читабельным:

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

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

C# 7 расширяет оператор switch для использования образцов в case-блоках:

В примере показан первый набор изменений в операторе switch.

  1. В операторе switch может использоваться переменная любого типа.
  2. Предложение case может указывать шаблон.
  3. Важен порядок предложений в case. Компилятор выдает ошибку, если предыдущий case соответствует базовому типу, а следующий case – соответствует производному типу.
  4. Все case-блоки содержат неявную проверку на null (**). В предыдущем примере, последний case-блок правилен, поскольку он будет срабатывать только тогда, когда аргумент не равен null.

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

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

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

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

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

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

📎📎📎📎📎📎📎📎📎📎