Udostępnij za pośrednictwem


Rekursywne dopasowywanie wzorca

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Kwestia mistrza: https://github.com/dotnet/csharplang/issues/45

Streszczenie

Rozszerzenia dopasowywania wzorców dla języka C# umożliwiają wiele zalet typów danych algebraicznych i dopasowywania wzorców z języków funkcjonalnych, ale w sposób, który płynnie integruje się z działaniem języka bazowego. Elementy tego podejścia są inspirowane powiązanymi funkcjami w językach programowania, takich jak F#, i Scala.

Szczegółowy projekt

Wyrażenie is

Operator is jest rozszerzony w celu przetestowania wyrażenia względem wzorca .

relational_expression
    : is_pattern_expression
    ;
is_pattern_expression
    : relational_expression 'is' pattern
    ;

Ta forma relational_expression jest dodatkiem do istniejących formularzy w specyfikacji języka C#. Jest to błąd czasu kompilacji, jeśli relational_expression z lewej strony tokenu is nie wyznacza wartości lub nie ma typu.

Każdy identyfikator wzorca wprowadza nową zmienną lokalną, która jest z pewnością przypisana po is operatora true (tj. z pewnością przypisana, kiedy jest prawdziwa).

Uwaga: istnieje technicznie niejednoznaczność między typem w is-expression a wzorcem stałym , z których każdy może być poprawną interpretacją kwalifikowanego identyfikatora. ** Staramy się powiązać go jako typ dla zgodności z poprzednimi wersjami języka; tylko wtedy, gdy to się nie powiedzie, rozwiązujemy to tak, jak wyrażenie w innych kontekstach, do pierwszego znalezionego elementu (który, musi być stałą lub typem). Ta niejednoznaczność jest obecna tylko po prawej stronie wyrażenia is.

Wzory

Wzorce są używane w operatorze is_pattern w switch_statementi w switch_expression do wyrażenia kształtu danych, względem których mają być porównywane dane przychodzące (które nazywamy wartością wejściową). Wzorce mogą być rekursywne, aby części danych mogły być dopasowane do subwzorców.

pattern
    : declaration_pattern
    | constant_pattern
    | var_pattern
    | positional_pattern
    | property_pattern
    | discard_pattern
    ;
declaration_pattern
    : type simple_designation
    ;
constant_pattern
    : constant_expression
    ;
var_pattern
    : 'var' designation
    ;
positional_pattern
    : type? '(' subpatterns? ')' property_subpattern? simple_designation?
    ;
subpatterns
    : subpattern
    | subpattern ',' subpatterns
    ;
subpattern
    : pattern
    | identifier ':' pattern
    ;
property_subpattern
    : '{' '}'
    | '{' subpatterns ','? '}'
    ;
property_pattern
    : type? property_subpattern simple_designation?
    ;
simple_designation
    : single_variable_designation
    | discard_designation
    ;
discard_pattern
    : '_'
    ;

Wzorzec deklaracji

declaration_pattern
    : type simple_designation
    ;

declaration_pattern sprawdza, czy wyrażenie jest danego typu, i przekształca je na ten typ, jeśli sprawdzenie zakończy się pomyślnie. Może to spowodować wprowadzenie lokalnej zmiennej danego typu o nazwie podanego identyfikatora, jeśli oznaczenie jest single_variable_designation. Ta zmienna lokalna na pewno jest przypisana, gdy wynik operacji dopasowywania wzorca jest true.

Semantyka wykonania tego wyrażenia polega na tym, że testuje wykonawczy typ lewego operandu relational_expression pod względem typu we wzorcu. Jeśli jest to ten typ środowiska uruchomieniowego (lub jakiś podtyp), a nie null, wynik is operator jest true.

Niektóre kombinacje statycznego typu po lewej stronie wyrażenia i danego typu są uznawane za niezgodne i prowadzą do błędu podczas kompilacji. Wartość typu, który jest statyczny, E, nazywana jest zgodną z wzorcem z typem T, jeśli istnieje konwersja tożsamości, niejawna konwersja odwołania, konwersja opakowania, jawna konwersja odwołań lub konwersja rozpakowywania z E na T, lub jeśli jeden z typów E lub Tjest typem otwartym. Jest to błąd czasu kompilacji, jeśli dane wejściowe typu E nie są zgodne z wzorcem typu z typem , z którym są porównywane.

Wzorzec typu jest przydatny do wykonywania testów typów w czasie wykonywania na typach referencyjnych i zastępuje idiom

var v = expr as Type;
if (v != null) { // code using v

Z nieco bardziej zwięzłym

if (expr is Type v) { // code using v

Jest to błąd, jeśli typ jest typem, który może przyjmować wartość null.

Wzorzec typu może być użyty do sprawdzania wartości typów nullable: wartość typu Nullable<T> (lub opakowana T) pasuje do wzorca typu T2 id, jeśli nie jest nullem, a typ T2 to T, lub jakiś podstawowy typ lub interfejs T. Na przykład w fragmentie kodu

int? x = 3;
if (x is int v) { // code using v

Warunek instrukcji if jest true podczas działania, a zmienna v przechowuje wartość 3 typu int w bloku. Po zakończeniu bloku zmienna v znajduje się w zakresie, ale nie jest jednoznacznie przypisana.

Wzorzec stałej

constant_pattern
    : constant_expression
    ;

Stały wzorzec sprawdza wartość wyrażenia względem stałej wartości. Stała może być dowolnym wyrażeniem stałym, takim jak literał, nazwa zadeklarowanej zmiennej const lub stała wyliczenia. Jeśli wartość wejściowa nie jest typem otwartym, wyrażenie stałe jest niejawnie konwertowane na typ dopasowanego wyrażenia; Jeśli typ wartości wejściowej nie jest zgodny ze wzorcem z typem wyrażenia stałego, operacja dopasowywania wzorca jest błędem.

Wzorzec c jest traktowany jako zgodny z przekonwertowaną wartością wejściową e, jeśli object.Equals(c, e) zwróci true.

Oczekujemy, że e is null stanie się najczęściej stosowanym sposobem testowania pod kątem null w nowo napisanym kodzie, ponieważ nie może ono wywołać zdefiniowanego przez użytkownika operator==.

Wzorzec zmiennej

var_pattern
    : 'var' designation
    ;
designation
    : simple_designation
    | tuple_designation
    ;
simple_designation
    : single_variable_designation
    | discard_designation
    ;
single_variable_designation
    : identifier
    ;
discard_designation
    : _
    ;
tuple_designation
    : '(' designations? ')'
    ;
designations
    : designation
    | designations ',' designation
    ;

Jeśli oznaczenie jest prostym_oznaczeniem, to wyrażenie e pasuje do wzorca. Innymi słowy, dopasowanie do wzorca var zawsze kończy się powodzeniem z simple_designation. Jeśli simple_designation jest single_variable_designation, to wartość e będzie powiązana z nowo utworzoną zmienną lokalną. Typ zmiennej lokalnej jest statycznym typem e.

Jeśli oznaczenie jest tuple_designation, wzorzec jest odpowiednikiem positional_pattern oznaczenia formularza, gdzie oznaczenies znajdują się w tuple_designation. Na przykład wzorzec var (x, (y, z)) jest odpowiednikiem (var x, (var y, var z)).

Jest to błąd, jeśli nazwa var wiąże się z typem.

Odrzuć wzorzec

discard_pattern
    : '_'
    ;

Wyrażenie e pasuje do wzorca _ zawsze. Innymi słowy każde wyrażenie pasuje do wzorca odrzucenia.

Wzorzec odrzucenia nie może być używany jako wzorzec w is_pattern_expression.

Wzorzec pozycyjny

Wzorzec pozycyjny sprawdza, czy wartość wejściowa nie jest null, wywołuje odpowiednią metodę Deconstruct i wykonuje dalsze dopasowywanie wzorca dla wartości wynikowych. Obsługuje również składnię wzorca przypominającą krotkę (bez podania typu), gdy typ wartości wejściowej jest taki sam jak typ zawierający Deconstruct, lub jeśli typ wartości wejściowej jest typem krotki, lub jeśli typ wartości wejściowej jest object lub ITuple i typ uruchomieniowy wyrażenia implementuje ITuple.

positional_pattern
    : type? '(' subpatterns? ')' property_subpattern? simple_designation?
    ;
subpatterns
    : subpattern
    | subpattern ',' subpatterns
    ;
subpattern
    : pattern
    | identifier ':' pattern
    ;

Jeśli pominięto typ , użyjemy go jako statycznego typu wartości wejściowej.

Biorąc pod uwagę dopasowanie wartości wejściowej do wzorca typusubpattern_list, metoda jest wybierana poprzez wyszukiwanie w typie w ramach dla dostępnych deklaracji i wybrania jednej spośród nich, używając tych samych reguł jak dla deklaracji dekonstrukcji.

Jest to błąd, jeśli positional_pattern pomija typ, zawiera tylko jeden podwzorzec bez identyfikatora , nie ma property_subpattern ani prostego oznaczenia . To rozróżnia między constant_pattern, który jest ujęty w nawiasy, a positional_pattern.

Aby wyodrębnić wartości, które mają być zgodne z wzorcami na liście,

  • Jeżeli typ został pominięty, a typ wartości wejściowej jest typem krotki, wymagana jest liczba podwzorców równa kardynalności krotki. Każdy z elementów krotki jest dopasowany do odpowiadającego podwzorca, a dopasowanie powiedzie się, jeśli wszystkie one powiedzie się. Jeśli jakikolwiek podwzorzec ma identyfikator , to musi oznaczać element krotki na odpowiadającej pozycji w typie krotki.
  • W przeciwnym razie, jeśli odpowiedni Deconstruct istnieje jako element członkowski typu , jest to błąd czasu kompilacji, jeśli typ wartości wejściowej nie jest zgodny ze wzorcem z typem . W czasie wykonywania wartość wejściowa jest testowana względem typu . Jeśli to się nie powiedzie, dopasowanie wzorca pozycyjnego zakończy się niepowodzeniem. Jeśli to się powiedzie, wartość wejściowa zostanie przekonwertowana na ten typ, a Deconstruct jest wywoływana ze świeżymi zmiennymi wygenerowanymi przez kompilator w celu odbierania parametrów out. Każda odebrana wartość jest dopasowywana do odpowiadającego jej podwzorca , a dopasowanie odnosi sukces, jeśli wszystkie te się powiodą. Jeśli jakikolwiek podwzorca ma identyfikator , to musi on nazwać parametr na odpowiedniej pozycji Deconstruct.
  • W przeciwnym razie, jeśli typ został pominięty, a wartość wejściowa jest typu object lub ITuple lub jakiegoś typu, który można przekonwertować na ITuple przez niejawną konwersję odwołania, i żaden identyfikator nie pojawia się wśród podwzorców, wówczas dopasowujemy za pomocą ITuple.
  • W przeciwnym razie wzorzec jest błędem czasu kompilacji.

Kolejność dopasowania podwzorców w czasie wykonywania jest nieokreślona, a nieudane dopasowanie może nie być zgodne ze wszystkimi podwzorcami.

Przykład

W tym przykładzie użyto wielu funkcji opisanych w tej specyfikacji

    var newState = (GetState(), action, hasKey) switch {
        (DoorState.Closed, Action.Open, _) => DoorState.Opened,
        (DoorState.Opened, Action.Close, _) => DoorState.Closed,
        (DoorState.Closed, Action.Lock, true) => DoorState.Locked,
        (DoorState.Locked, Action.Unlock, true) => DoorState.Closed,
        (var state, _, _) => state };

Wzorzec właściwości

Wzorzec właściwości sprawdza, czy wartość wejściowa nie jest null i rekursywnie pasuje do wartości wyodrębnionych przy użyciu dostępnych właściwości lub pól.

property_pattern
    : type? property_subpattern simple_designation?
    ;
property_subpattern
    : '{' '}'
    | '{' subpatterns ','? '}'
    ;

Jest to błąd, jeśli jakikolwiek podwzorzec wzorca_właściwości nie zawiera identyfikatora (musi być w drugiej formie, która zawiera identyfikator ). Końcowy przecinek po ostatnim podwzorcu jest opcjonalny.

Należy pamiętać, że wzorzec sprawdzania wartości null wypada ze wzorca właściwości trywialnych. Aby sprawdzić, czy ciąg s jest inny niż null, możesz napisać dowolną z poniższych formularzy

if (s is object o) ... // o is of type object
if (s is string x) ... // x is of type string
if (s is {} x) ... // x is of type string
if (s is {}) ...

Biorąc pod uwagę dopasowanie wyrażenia e do typu wzorca{property_pattern_list}, jest to błąd czasu kompilacji, jeśli wyrażenie e enie jest zgodne ze wzorcem z typem T wyznaczonym przez typ . Jeśli typ jest nieobecny, uznajemy to za statyczny typ i. Jeśli identyfikatora jest obecny, deklaruje zmienną wzorca typu . Każdy z identyfikatorów pojawiających się po lewej stronie jego property_pattern_list musi oznaczać dostępną właściwość do odczytu lub pole T. Jeśli obecne jest simple_designationproperty_pattern, definiuje zmienną wzorca typu T.

W czasie wykonywania wyrażenie jest testowane względem T. Jeśli to się nie powiedzie, dopasowanie wzorca właściwości zakończy się niepowodzeniem, a wynik zostanie false. Jeśli to się powiedzie, każde pole lub właściwość property_subpattern jest odczytywane i jego wartość dopasowana do odpowiadającego mu wzorca. Wynik całego dopasowania to false tylko wtedy, gdy wynik któregoś z nich to false. Kolejność dopasowania podwzorców nie jest określona, a dopasowanie nie powiodło się, może nie być zgodne ze wszystkimi podwzorcami w czasie wykonywania. Jeśli dopasowanie powiedzie się, a simple_designationproperty_pattern jest single_variable_designation, definiuje zmienną typu T przypisaną do dopasowanej wartości.

Uwaga: wzorzec właściwości może służyć do dopasowywania wzorca do typów anonimowych.

Przykład
if (o is string { Length: 5 } s)

Wyrażenie przełącznika

Dodano switch_expression, aby obsługiwać semantykę podobną do switchw kontekście wyrażenia.

Składnia języka C# jest rozszerzona o następujące produkcje składniowe:

multiplicative_expression
    : switch_expression
    | multiplicative_expression '*' switch_expression
    | multiplicative_expression '/' switch_expression
    | multiplicative_expression '%' switch_expression
    ;
switch_expression
    : range_expression 'switch' '{' '}'
    | range_expression 'switch' '{' switch_expression_arms ','? '}'
    ;
switch_expression_arms
    : switch_expression_arm
    | switch_expression_arms ',' switch_expression_arm
    ;
switch_expression_arm
    : pattern case_guard? '=>' expression
    ;
case_guard
    : 'when' null_coalescing_expression
    ;

switch_expression nie jest dozwolona jako expression_statement.

Rozważamy złagodzenie tego w przyszłej rewizji.

Typ switch_expression to najbardziej typowy typ (§12.6.3.15) wyrażeń wyświetlanych po prawej stronie => tokenów switch_expression_arms, jeśli taki typ istnieje, a wyrażenie w każdym ramieniu wyrażenia przełącznika może zostać niejawnie przekonwertowane na ten typ. Ponadto dodajemy nową konwersję wyrażenia przełącznikowego , która jest wstępnie zdefiniowaną niejawną konwersją każdego wyrażenia przełącznikowego na typ T, dla którego istnieje niejawna konwersja każdej gałęzi na T.

Jest to błąd, jeśli jakiś wzorzec switch_expression_armnie może wpłynąć na wynik, ponieważ jakiś poprzedni wzorzec i ochrona będą zawsze zgodne.

Mówi się, że wyrażenie przełącznika jest wyczerpujące, jeśli któreś z ramion wyrażenia przełącznika obsługuje każdą wartość jego wejścia. Kompilator generuje ostrzeżenie, jeśli wyrażenie przełącznika nie jest wyczerpujące.

W czasie wykonywania wynikiem switch_expression jest wartość wyrażenia pierwszego switch_expression_arm, dla którego wyrażenie po lewej stronie switch_expression jest zgodne ze wzorcem switch_expression_armi dla którego case_guardswitch_expression_arm, jeśli jest obecny, oblicza wartość true. Jeśli nie ma takiego switch_expression_arm, switch_expression zgłasza wystąpienie wyjątku System.Runtime.CompilerServices.SwitchExpressionException.

Opcjonalne nawiasy podczas przełączania na literał krotki

Aby włączyć literał krotki, używając switch_statement, musisz napisać, co wydaje się być zbędnymi nawiasami.

switch ((a, b))
{

Aby zezwolić

switch (a, b)
{

nawiasy instrukcji switch są opcjonalne, gdy włączone wyrażenie jest literałem krotki.

Kolejność oceny w dopasowywaniu wzorców

Zapewnienie elastyczności kompilatora w zmienianiu kolejności operacji wykonywanych podczas dopasowywania wzorców może pozwolić na elastyczność, która może służyć do poprawy wydajności dopasowywania wzorców. Wymaganie (niewymuszone) oznaczałoby, że właściwości dostępne we wzorcu i metody Dekonstrukcji powinny być "czyste" (bez efektów ubocznych, idempotentne itp.). Nie oznacza to, że dodalibyśmy czystość jako koncepcję języka, ale pozwolilibyśmy na elastyczność kompilatora w operacjach zmiany kolejności.

Uchwała 2018-04-04 LDM: potwierdzono, że kompilator może zmieniać kolejność wywołań do Deconstruct, dostępów do właściwości i wywołań metod w ITupleoraz może zakładać, że zwracane wartości są takie same przy wielu wywołaniach. Kompilator nie powinien wywoływać funkcji, które nie mogą mieć wpływu na wynik, i będziemy bardzo ostrożni przed wprowadzeniem jakichkolwiek zmian w kolejności generowania oceny kompilatora w przyszłości.

Niektóre możliwe optymalizacje

Kompilacja dopasowywania wzorców może korzystać z wspólnych części wzorców. Jeśli na przykład test typu najwyższego poziomu dwóch kolejnych wzorców w switch_statement jest tego samego typu, wygenerowany kod może pominąć test typu dla drugiego wzorca.

Gdy niektóre wzorce są liczbami całkowitymi lub ciągami, kompilator może wygenerować ten sam rodzaj kodu, który generuje dla instrukcji switch we wcześniejszych wersjach języka.

Aby uzyskać więcej informacji na temat tego rodzaju optymalizacji, zobacz [Scott i Ramsey (2000)].