Dela via


Rekursiv mönstermatchning

Notera

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningarna (Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Championfråga: https://github.com/dotnet/csharplang/issues/45

Sammanfattning

Mönstermatchningstillägg för C# möjliggör många av fördelarna med algebraiska datatyper och mönstermatchning från funktionella språk, men på ett sätt som smidigt integreras med känslan av det underliggande språket. Element i den här metoden är inspirerade av relaterade funktioner i programmeringsspråken F# och Scala.

Detaljerad design

Är uttryck

Operatorn is utökas för att testa ett uttryck mot ett mönster.

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

Den här typen av relational_expression är utöver de befintliga formulären i C#-specifikationen. Det är ett kompileringsfel om relational_expression till vänster om is token inte anger något värde eller inte har någon typ.

Varje identifierare i mönstret introducerar en ny lokal variabel som är definitivt tilldelad efter att is-operatorn har true (dvs. definitivt tilldelad när värdet är sant).

Obs! Det finns tekniskt sett en tvetydighet mellan typ i en is-expression och constant_pattern, vilket kan vara en giltig pars av en kvalificerad identifierare. Vi försöker binda den som en typ för kompatibilitet med tidigare versioner av språket. endast om det misslyckas löser vi det som vi gör ett uttryck i andra kontexter, till det första som hittas (som måste vara antingen en konstant eller en typ). Den här tvetydigheten finns bara på högra sidan av ett is-uttryck.

Mönster

Mönster används i operatorn is_pattern, i en switch_statementoch i en switch_expression för att representera datans struktur, mot vilket inkommande data (som vi kallar indata) ska jämföras. Mönster kan vara rekursiva så att delar av data kan matchas mot undermönster.

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
    : '_'
    ;

Deklarationsmönster

declaration_pattern
    : type simple_designation
    ;

declaration_pattern testar att ett uttryck är av en specificerad typ och gör det till den typen om testet lyckas. Detta kan introducera en lokal variabel av den angivna typen med namnet av den angivna identifieraren, om beteckningen är en single_variable_designation. Den lokala variabeln är definitivt tilldelad när resultatet av mönstermatchningsåtgärden är true.

Körtidssemantiken för det här uttrycket är att den testar körtidstypen för den vänstra relational_expression operandens typ mot typen i mönstret. Om det är av den exekveringstypen (eller någon undertyp) och inte är null, är resultatet av is operatortrue.

Vissa kombinationer av statisk typ av vänster sida och den angivna typen anses vara inkompatibla och resulterar i kompileringsfel. Ett värde av statisk typ E sägs vara mönsterkompatibel med en typ T om det finns en identitetskonvertering, en implicit referenskonvertering, en boxningskonvertering, en explicit referenskonvertering eller en avboxningskonvertering från E till Teller om någon av dessa typer är en öppen typ. Det är ett kompileringsfel om indata av typen E inte är mönsterkompatibla med typ i ett typmönster som det matchas med.

Typmönstret är användbart för att utföra körningstyptester av referenstyper och ersätter formspråket

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

Med den något mer koncisa texten

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

Det är ett fel om typ är en nullbar värdetyp.

Typmönstret kan användas för att testa värden av null-typer: ett värde av typen Nullable<T> (eller en rutad T) matchar ett typmönster T2 id om värdet inte är null och typen av T2 är Teller någon bastyp eller gränssnitt för T. Till exempel i kodfragmentet

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

Villkoret för if-instruktionen är true vid körning och variabeln v innehåller värdet 3 av typen int inom blocket. Efter blocket är variabeln v i scope men inte garanterat tilldelad.

Konstant mönster

constant_pattern
    : constant_expression
    ;

Ett konstant mönster testar värdet för ett uttryck mot ett konstant värde. Konstanten kan vara ett konstant uttryck, till exempel en literal, namnet på en deklarerad const variabel eller en uppräkningskonstant. När indatavärdet inte är en öppen typ konverteras konstantuttrycket implicit till typen för det matchade uttrycket. Om typen av indatavärde inte är mönsterkompatibel med typen av konstant uttryck är mönstermatchningsåtgärden ett fel.

Mönstret c anses matcha det konverterade indatavärdet e om object.Equals(c, e) skulle returnera true.

Vi förväntar oss att se e is null som det vanligaste sättet att testa för null i nyskriven kod, eftersom det inte kan anropa en användardefinierad operator==.

Var-mönster

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
    ;

Om -beteckningen är en simple_designationmatchar ett uttryck e mönstret. Med andra ord lyckas en matchning till ett var-mönster alltid med en enkel_beteckning. Om simple_designation är en single_variable_designation, är värdet för e bundet till en nyligen introducerad lokal variabel. Typen av lokal variabel är den statiska typen av e.

Om beteckning är en tuple_designation, motsvarar mönstret ett mönster av typen positional_pattern av formuläret (varbeteckning, ... ) där beteckningarnaär de som finns inom tuple_designation. Mönstret var (x, (y, z)) motsvarar till exempel (var x, (var y, var z)).

Det är ett fel om namnet var binder till en typ.

Ignorera mönster

discard_pattern
    : '_'
    ;

Ett uttryck e matchar alltid mönstret _. Med andra ord matchar varje uttryck ignoreringsmönstret.

Ett ignorerandemönster får inte användas som mönster för en is_pattern_expression.

Positionsmönster

Ett positionsmönster kontrollerar att indatavärdet inte är null, anropar en lämplig Deconstruct metod och utför ytterligare mönstermatchning på de resulterande värdena. Den stöder också en tupplarliknande mönstersyntax (utan att typen anges) när typen av indatavärde är samma som den typ som innehåller Deconstruct, eller om typen av indatavärde är en tupplartyp, eller om typen av indatavärde är object eller ITuple och körningstypen för uttrycket implementerar ITuple.

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

Om den typen utelämnas tar vi det som den statiska typen av indatavärde.

Med tanke på en matchning av ett indatavärde med mönstret typ(subpattern_list), väljs en metod genom att söka i typ efter tillgängliga deklarationer av Deconstruct och välja en av dem med samma regler som för dekonstruktionsdeklarationen.

Det är ett fel om en positional_pattern utelämnar typen, har en enda subpattern utan en identifierare, inte har någon property_subpattern och har ingen simple_designation. Detta skiljer sig mellan en constant_pattern som är parentesiserad och en positional_pattern.

För att extrahera de värden som ska matchas mot mönstren i listan,

  • Om typ utelämnades och indatavärdets typ är en tuppelns typ måste antalet undermönster vara detsamma som tuppelns kardinalitet. Varje tuppelelement matchas mot motsvarande delmönster, och matchningen lyckas om alla lyckas. Om något delmönster har en identifierare, då måste det namnge ett tupleelement på motsvarande position i tupletypen.
  • Om det annars finns en lämplig Deconstruct som medlem av typär det ett kompileringsfel om typen av indatavärde inte är mönsterkompatibel med typ. Vid körningen testas indatavärdet mot typ. Om detta misslyckas misslyckas positionsmönstermatchningen. Om det lyckas konverteras indatavärdet till den här typen och Deconstruct anropas med nya kompilatorgenererade variabler för att ta emot parametrarna för out. Varje värde som mottogs matchas mot motsvarande delmönster, och matchningen lyckas om alla dem lyckas. Om något delmönster har en identifierare, måste det namnge en parameter på motsvarande position i Deconstruct.
  • Annars, om typen utelämnades och indatavärdet är av typen object eller ITuple eller någon typ som kan konverteras till ITuple genom en implicit referenskonvertering, och ingen identifierare visas bland submönstren, matchar vi genom ITuple.
  • Annars är mönstret ett kompileringsfel.

Ordningen i vilken undermönster matchas vid körning är ospecificerad, och en misslyckad matchning behöver inte nödvändigtvis försöka matcha alla undermönster.

Exempel

I det här exemplet används många av de funktioner som beskrivs i den här specifikationen

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

Egenskapsmönster

Ett egenskapsmönster kontrollerar att indatavärdet inte är null och matchar rekursivt värden som extraheras med hjälp av tillgängliga egenskaper eller fält.

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

Det är ett fel om något delmönster av ett property_pattern inte innehåller en identifierare (det måste vara i den andra formen, som har en identifierare). Ett avslutande kommatecken efter det sista underavsnittet är valfritt.

Observera att ett null-kontrollmönster faller ur ett trivialt egenskapsmönster. Om du vill kontrollera om strängen s inte är null kan du skriva något av följande formulär

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 {}) ...

Givet en matchning av ett uttryck e till mönstret typ{property_pattern_list}är det ett fel vid kompileringstiden om uttrycket e inte är mönsterkompatibelt med typen T som anges av typ. Om typen saknas tar vi det som den statiska typen av e. Om identifierare finns deklarerar den en mönstervariabel av typen typ. Var och en av identifierarna som visas till vänster i dess property_pattern_list måste ange en tillgänglig läsbar egenskap eller ett fält för T-. Om simple_designation för property_pattern finns definierar den en mönstervariabel av typen T.

Under körning testas uttrycket mot T. Om detta misslyckas så misslyckas även egenskapsmönstermatchningen och resultatet är false. Om det lyckas läss varje property_subpattern fält eller egenskap och dess värde matchas mot motsvarande mönster. Resultatet av hela matchningen är bara false om resultatet av något av dessa är false. Ordningen i vilken undermönster matchas specificeras inte, och en misslyckad matchning kan leda till att inte alla undermönster matchas vid körning. Om matchningen lyckas och simple_designation av property_pattern är en single_variable_designation, definieras en variabel av typen T som tilldelas det matchade värdet.

Obs! Egenskapsmönstret kan användas för att mönstermatcha med anonyma typer.

Exempel
if (o is string { Length: 5 } s)

Växla uttryck

En switch_expression läggs till för att stödja switch-liknande semantik i en uttryckskontext.

C#-språksyntaxen utökas med följande syntaktiska produktioner:

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 tillåts inte som en expression_statement.

Vi tittar på att lätta på detta i en framtida revision.

Typen av switch_expression är den vanligaste typen (§12.6.3.15) av uttrycken som visas till höger om => token för switch_expression_arms om en sådan typ finns och uttrycket i varje arm i switchuttrycket implicit kan konverteras till den typen. Dessutom lägger vi till en ny växla uttryckskonvertering, som är en fördefinierad implicit konvertering från ett switch-uttryck till varje typ T där det finns en implicit konvertering från varje arm-uttryck till T.

Det är ett fel om vissa switch_expression_armmönster inte kan påverka resultatet eftersom något tidigare mönster och skydd alltid matchar.

Ett växeluttryck sägs vara uttömmande om någon del av växeluttrycket hanterar varje värde i indata. Kompilatorn ska utfärda en varning om ett växeluttryck inte är uttömmande.

Vid körning är resultatet av switch_expression värdet för det uttrycket för den första switch_expression_arm där uttrycket till vänster i switch_expression matchar mönstret switch_expression_armoch för vilket case_guard av switch_expression_arm, om det finns utvärderas till true. Om det inte finns någon sådan switch_expression_armgenererar switch_expression en instans av undantaget System.Runtime.CompilerServices.SwitchExpressionException.

Valfria parens vid växling på en tuppelliteral

För att kunna växla till en tuppelliteral med hjälp av switch_statementmåste du skriva vad som verkar vara redundanta parens

switch ((a, b))
{

Att tillåta

switch (a, b)
{

parenteserna för switch-instruktionen är valfria när det uttryck som byts ut är en tuppelliteral.

Utvärderingsordning i mönstermatchning

Om du ger kompilatorn flexibilitet när det gäller att ordna om de åtgärder som körs under mönstermatchning kan du tillåta flexibilitet som kan användas för att förbättra effektiviteten i mönstermatchning. Kravet (oforcerat) skulle vara att egenskaper som används i ett mönster och dekonstruktionsmetoderna måste vara "rena" (sidoeffektfria, idempotenta, osv.). Det betyder inte att vi skulle lägga till renhet som ett språkkoncept, bara att vi skulle tillåta kompilatorns flexibilitet vid omordningsåtgärder.

Resolution 2018-04-04 LDM: bekräftad: kompilatorn får ordna om anrop till Deconstruct, egenskapsåtkomster och anrop av metoder i ITupleoch kan anta att returnerade värden är samma från flera anrop. Kompilatorn bör inte anropa funktioner som inte kan påverka resultatet, och vi kommer att vara mycket försiktiga innan vi gör några ändringar i den kompilatorgenererade utvärderingsordningen i framtiden.

Några möjliga optimeringar

Kompilering av mönstermatchning kan dra nytta av vanliga delar av mönster. Om till exempel toppnivåtyptestet av två efterföljande mönster i en switch_statement är av samma typ kan den genererade koden hoppa över typtestet för det andra mönstret.

När vissa av mönstren är heltal eller strängar kan kompilatorn generera samma typ av kod som den genererar för en switch-instruktion i tidigare versioner av språket.

Mer information om den här typen av optimeringar finns i [Scott och Ramsey (2000)].