Изменения сопоставления шаблонов для C# 9.0
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Мы рассмотрим небольшое количество улучшений для сопоставления шаблонов для C# 9.0, которые имеют естественную согласованность и хорошо работают над рядом распространенных проблем программирования:
- Шаблоны типов https://github.com/dotnet/csharplang/issues/2925
- https://github.com/dotnet/csharplang/issues/1350 скобочные паттерны для обеспечения или усиления приоритета новых комбинаторов
-
https://github.com/dotnet/csharplang/issues/1350 Сочетательные
and
шаблоны, требующие сопоставления как двух различных шаблонов; -
https://github.com/dotnet/csharplang/issues/1350 дизъюнктивные
or
шаблоны, которые требуют совпадения с любым из двух различных шаблонов; -
https://github.com/dotnet/csharplang/issues/1350 отрицательные
not
шаблоны, которые требуют, чтобы заданный шаблон не соответствовал; и - https://github.com/dotnet/csharplang/issues/812 реляционные шаблоны, требующие, чтобы входное значение было меньше, меньше или равно и т. д. заданной константе.
Шаблоны в скобках
Круглые скобки позволяют программисту помещать скобки вокруг любого шаблона. Это не так полезно с существующими шаблонами в C# 8.0, однако новые комбинаторы шаблонов вводят приоритет, который программист может захотеть переопределить.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Шаблоны типов
Мы разрешаем использовать тип как шаблон:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
Это ретконит существующее тип-выражение в шаблон-выражение, в котором шаблон — это тип-шаблон, хотя мы не будем изменять дерево синтаксиса, созданное компилятором.
Одна из тонких проблем реализации заключается в том, что эта грамматика неоднозначна. Строку, например a.b
, можно проанализировать либо как полное имя (в контексте типа), либо как точечное выражение (в контексте выражения). Компилятор уже может обрабатывать полное имя так же, как пунктирное выражение, чтобы обрабатывать что-то подобное e is Color.Red
. Семантический анализ компилятора будет дополнительно расширен, чтобы иметь возможность привязки шаблона константы (синтаксического) (например, пунктичного выражения) в качестве типа, чтобы рассматривать его как шаблон привязанного типа для поддержки этой конструкции.
После этого изменения вы сможете написать
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
Реляционные шаблоны
Реляционные шаблоны позволяют программисту выразить, что входное значение должно соответствовать реляционному ограничению при сравнении с константным значением:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
Реляционные шаблоны поддерживают реляционные операторы <
, <=
, >
и >=
на всех встроенных типах, поддерживающих такие двоичные реляционные операторы с двумя операндами одного типа в выражении. В частности, мы поддерживаем все эти реляционные шаблоны для sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
и nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
Выражение требуется для вычисления константного значения. Это ошибка, если это константное значение double.NaN
или float.NaN
. Это ошибка, если выражение является константой NULL.
Если входные данные являются типом, для которого определен подходящий встроенный двоичный реляционный оператор, который применим к входным данным в качестве левого операнда и заданной константы в качестве правого операнда, оценка этого оператора принимается в качестве значения реляционного шаблона. В противном случае мы преобразуем входные данные в тип выражения с помощью явного значения NULL или распаковки преобразования. Это ошибка во время компиляции, если такого преобразования нет. Считается, что шаблон не соответствует, если преобразование не удалось. Если преобразование завершается успешно, результат операции сопоставления шаблонов является результатом оценки выражения e OP v
, где e
преобразованные входные данные, OP
является реляционным оператором, и v
является константным выражением.
Комбинаторы шаблонов
Шаблоны комбинаторы разрешают сопоставление двух разных шаблонов с помощью and
(это можно расширить до любого количества шаблонов путем многократного использования and
), двух разных шаблонов с помощью or
(ditto) или отрицания шаблона с помощью not
.
Обычное использование комбинатора - это идиома.
if (e is not null) ...
Более читаемый, чем текущий идиом e is object
, этот шаблон четко выражает, что он проверяет наличие ненулевого значения.
and
и or
комбинаторы будут полезны для тестирования диапазонов значений
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
В этом примере показано, что and
будет иметь более высокий приоритет синтаксического анализа (т. е. привязывается более тесно), чем or
. Программист может использовать заключенный в скобки шаблон , чтобы сделать приоритет явным.
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Как и все шаблоны, эти комбинаторы можно использовать в любом контексте, в котором ожидается шаблон, включая вложенные шаблоны, — выражение шаблона, — выражение переключенияи шаблон метки case оператора switch.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Изменение на 6.2.5: Неоднозначности грамматики
Из-за внедрения шаблона типа универсальный тип может появляться перед маркером =>
. Поэтому мы добавим =>
в набор маркеров, перечисленных в §6.2.5 Грамматическая неоднозначность, чтобы разрешить неоднозначность <
, которая начинается со списка аргументов типа. См. также https://github.com/dotnet/roslyn/issues/47614.
Открытые проблемы с предлагаемыми изменениями
Синтаксис для реляционных операторов
Являются ли and
, or
и not
какое-то контекстное ключевое слово? В этом случае существует критическое изменение (например, по сравнению с их использованием в качестве указателя в шаблоне декларации).
Семантика (например, тип) для реляционных операторов
Мы ожидаем поддерживать все примитивные типы, которые можно сравнить в выражении с помощью реляционного оператора. Значение в простых случаях ясно
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Но когда входные данные не являются таким примитивным типом, какой тип мы пытаемся преобразовать его в?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
Мы предложили, что если тип входных данных уже является сопоставимым примитивом, то есть тип сравнения. Однако, если ввод не является сопоставимым примитивом, реляционное выражение рассматривается как включающее неявную проверку типа с типом константы на правой стороне реляционного выражения. Если программист намерен поддерживать несколько типов входных данных, это необходимо сделать явным образом:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Результат: Реляционный оператор включает в себя неявную проверку типа на тип константы на правой стороне выражения.
Поток информации о типе слева направо от and
Было предложено, что при написании комбинатора and
информация о верхнем уровне типа, полученная слева, может передаваться направо. Например
bool isSmallByte(object o) => o is byte and < 100;
Здесь тип ввода второго шаблона сужается типом, сужающим требований слева от and
. Мы определим семантику сужения типов для всех шаблонов следующим образом.
узкий тип шаблона P
определяется следующим образом:
- Если
P
является шаблоном типа, то суженный тип является типом этого шаблона. - Если
P
является шаблоном объявления, то сузившийся тип , обозначенный как, является типом шаблона объявления. - Если
P
является рекурсивным шаблоном, который предоставляет явный тип, то суженный тип является этим типом. - Если
P
соответствует с помощью правил дляITuple
, суженный тип — это, который является типомSystem.Runtime.CompilerServices.ITuple
. - Если
P
является константным шаблоном, где константа не равна NULL, и если выражение не имеет преобразования константного выражения в тип ввода , то суженный тип является типом этой константы. - Если
P
является реляционным шаблоном, в котором постоянное выражение не имеет преобразования постоянных выражений в тип данных , то тип сужения является типом константы. - Если
P
является шаблономor
, суженный тип является общим типом суженного типа подшаблонов, если такой общий тип существует. Для этого алгоритм общего типа рассматривает только идентичности, упаковку и неявные преобразования ссылок, а также рассматривает все подпаттерны последовательности шаблоновor
(игнорируя круглые скобки). - Если
P
является шаблономand
, то суженный тип является суженным типом правого шаблона. Кроме того, узкий тип левого шаблона является типом ввода правого шаблона. - В противном случае узкий тип
P
является входным типомP
.
Результат. Реализована приведенная выше семантика сужения.
Определения переменных и определенное назначение
Добавление шаблонов or
и not
создает некоторые интересные новые проблемы вокруг переменных шаблонов и определенного назначения. Поскольку переменные обычно могут быть объявлены не более чем один раз, кажется, что любая шаблонная переменная, объявленная на одной стороне шаблона or
, не будет явно присвоена при совпадении шаблона. Аналогичным образом, переменная, объявленная внутри шаблона not
, не ожидается, что будет явно назначена при совпадении шаблона. Самый простой способ решения этой проблемы — запретить объявлять переменные шаблона в этих контекстах. Однако это может быть слишком строгим. Существуют и другие подходы к рассмотрению.
Один из сценариев, которые стоит рассмотреть, это
if (e is not int i) return;
M(i); // is i definitely assigned here?
Это не работает сегодня, так как для является выражением шаблона, переменные шаблона считаются определенно назначены только в том случае, если является выражением шаблона имеет значение true ("определенно назначено при значении true").
Поддержка этого будет проще (с точки зрения программиста), чем добавление поддержки для оператора с негированным условием if
. Даже если мы добавим такую поддержку, программисты будут задаваться вопросом, почему приведенный выше фрагмент не работает. С другой стороны, тот же сценарий в switch
имеет меньше смысла, так как в программе нет соответствующей точки, где определенно назначено, когда ложные будут значимыми. Можно ли допустить это в случае как выражение шаблона, но не в других контекстах, где разрешены шаблоны? Это кажется нерегулярным.
Это проблема определенного назначения в дисъюнктив-шаблон.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Мы бы ожидали, что i
будет однозначно определено, если входные данные не равны нулю. Но так как мы не знаем, равняется ли входное значение нулю внутри блока, i
определенно не назначено. Однако как поступить, если мы разрешаем i
объявлять в разных взаимоисключающих шаблонах?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
Здесь переменная i
определенно назначается внутри блока и принимает значение из другого элемента кортежа при обнаружении нулевого элемента.
Также было предложено разрешить переменным иметь возможность быть многократно определенными в каждом случае блока case:
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Чтобы сделать любую из этих работ, необходимо тщательно определить, где разрешены такие несколько определений, и в каких условиях такая переменная считается определенно назначенной.
Если мы решим отложить такую работу до более позднего времени (что я советую), мы могли бы сказать в C# 9.
- под
not
илиor
переменные шаблона могут не объявляться.
Тогда у нас было бы время, чтобы приобрести некоторый опыт, который обеспечит понимание возможной пользы от смягчения этого в будущем.
Результат: Переменные шаблона не могут быть объявлены под шаблоном not
или or
.
Диагностика, подчинение и исчерпанность
Эти новые формы шаблонов представляют множество новых возможностей для диагностики ошибки программиста. Нам потребуется решить, какие типы ошибок мы будем диагностировать, и как это сделать. Ниже приведены некоторые примеры.
case >= 0 and <= 100D:
Этот случай никогда не может совпадать (поскольку входные данные не могут быть как int
, так и double
). У нас уже есть ошибка, когда мы обнаруживаем случай, который никогда не может совпадать, но его формулировка ("Вариант переключателя уже учтен предыдущим случаем" и "Шаблон уже обработан предыдущей ветвью выражения переключателя") может вводить в заблуждение в новых сценариях. Возможно, нам придется изменить слово, чтобы просто сказать, что шаблон никогда не будет соответствовать входным данным.
case 1 and 2:
Аналогичным образом это была бы ошибка, так как значение не может быть как 1
, так и 2
.
case 1 or 2 or 3 or 1:
Этот случай можно сопоставить, но or 1
в конце не добавляет смысла в шаблон. Я предлагаю стремиться к созданию ошибки всякий раз, когда какая-либо конъюнкция или дизъюнкция составного шаблона не определяет переменную шаблона или не влияет на набор сопоставленных значений.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
Здесь 0 or 1 or
ничего не добавляет ко второму случаю, так как первый случай обработал бы эти значения. Это тоже заслуживает называться ошибкой.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Выражение коммутатора, например это, должно считаться исчерпывающим (обрабатывает все возможные входные значения).
В C# 8.0 выражение коммутатора с вводом типа byte
считается исчерпывающим, если он содержит окончательный объект, шаблон которого соответствует всему (отмена шаблона или var-pattern). Даже выражение switch, имеющее ветвь для каждого отдельного значения byte
, не считается исчерпывающим в C# 8. Чтобы правильно справиться с исчерпывающими реляционными шаблонами, нам также придется обработать этот случай. Это технически будет критическим изменением, но пользователь, скорее всего, не замечает.
C# feature specifications