Разбираем Сопоставления по Шаблону в C#. Часть 5/5
Части 1, 2, 3, 4
Шаблоны списка и среза
Проверяют последовательность поэлементно.
[1, 2, 3] соответствует массиву длиной 3 с точно такими же значениями.Два вспомогательных элемента выполняют основную работу:
- Шаблон среза
.. соответствует нулю или более элементам.- Шаблон отбрасывания
_ соответствует одному элементу.Например:
-
[_, >0, ..] аналогично array.Length >= 2 && array[1] > 0.-
[.., <=0, _] аналогично array.Length >= 2 && array[^2] <= 0.Шаблон списка может содержать не более одного среза. Использование двух срезов для поиска элемента в любом месте массива не допускается.
Типичный пример использования — алгоритмы с малым количеством элементов, где требуются именованные локальные переменные для первых нескольких элементов:
double Sum(ReadOnlySpan<double> vector) {
switch (vector) {
case []:
return 0;
case [double x]:
return x;
case [double x, double y]:
return x + y;
default:
double sum = 0;
for (int i = 0; i < vector.Length; ++i) {
double v = vector[i];
sum += v;
}
return sum;
};
}Входными данными для шаблона списка может быть любой элемент, который одновременно является счётным (имеет свойство Length или Count) и индексируемым (поддерживает индексатор и форму
^ - счёт от конца - из операторов индекса и диапазона). Это включает массивы, List<T>, Span<T>, ReadOnlySpan<T>, строки и любой пользовательский тип, который предоставляет необходимые элементы. Шаблоны списков рекурсивные. Элементы шаблона сами могут быть шаблонами:
bool EndsWithListOfOne(List<List<int>> list) =>
list is [.., [_]];
Рекурсивная природа делает шаблоны списков эффективными для анализа полуструктурированных данных, таких как строки CSV-файлов, где некоторые строки содержат больше столбцов, чем другие. Вы сопоставляете данные по форме строки, а не вручную записываете арифметические операции с индексами.
Ловушка выделения памяти
Шаблон var может захватывать результат среза, как в
[.. var nMinusOneFirstElems, 37]. Не делайте этого в горячих путях. Компилятор под капотом аллоцирует массив для материализации среза. То же касается строк. Если вы обрабатываете миллионы элементов, выделение памяти на каждое совпадение приведёт к сбою выполнения. Либо откажитесь от захвата среза, либо переключитесь на Span<T>, чтобы компилятор мог избежать выделения памяти.Известные ограничения
1. Внутри шаблона могут появляться только константы времени компиляции. Значения из конфигурации, базы данных и вычисления времени выполнения не могут. Это главная причина, по которой сопоставление с образцом не используется чаще в бизнес-коде.
2. Предложение when ограничено оператором switch. Вы не можете прикрепить when к оператору if.
3. Шаблон среза может появляться не более одного раза в шаблоне списка.
4. Шаблоны списков с захваченными срезами могут вызывать скрытое выделение памяти.
5. Проверка исчерпываемости не всегда работает. Выражение switch над замкнутой иерархией классов не является доказуемо исчерпывающим для компилятора так, как, например, дискриминированное объединение в F#.
Когда не использовать
Пример ниже с вычислением площади в иерархии классов фигур правильный синтаксически, но демонстрирет антипаттерн:
public static double Area(this Shape shape) =>
shape switch {
Circle c => c.Radius * c.Radius * Math.PI,
Rectangle r => r.Width * r.Height
};
Добавление каждой новой фигуры заставляет искать каждый такой оператор switch в коде. Это классическое нарушение принципа открытости/закрытости. Хуже того, таблица виртуальных методов работает быстрее, чем многократные проверки совместимости типов во время выполнения. Абстрактное свойство Area в базовом классе Shape выигрывает как в плане удобства сопровождения, так и в производительности.
Сопоставление с образцом оправдано, когда:
- Типы, которые вы используете для оператора switch, не связаны наследованием (см. пример с Count в шаблоне типа).
- Вы декодируете структурированные данные, например, парсер или конечный автомат.
- Вы пишете конечный фрагмент условной логики над примитивами, где полиморфизм был бы излишним.
Если вы обнаружите, что добавляете третий тип в оператор switch в объекте предметной области, остановитесь и спросите себя, не является ли правильным решением виртуальный метод.
Источник: https://blog.ndepend.com/c-pattern-matching-explained/