Dela via


Mönstermatchningsändringar för C# 9.0

Obs!

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 LDM-mötesanteckningar (Language Design Meeting)som är relevanta.

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

Vi överväger en liten handfull förbättringar av mönstermatchning för C# 9.0 som har naturlig synergi och fungerar bra för att hantera ett antal vanliga programmeringsproblem:

Parenteserade mönster

Parenteserade mönster gör det möjligt för programmeraren att placera parenteser runt valfritt mönster. Detta är inte så användbart med de befintliga mönstren i C# 8.0, men de nya mönsterkombinatorerna ger en prioritet som programmeraren kanske vill åsidosätta.

primary_pattern
    : parenthesized_pattern
    | // all of the existing forms
    ;
parenthesized_pattern
    : '(' pattern ')'
    ;

Typmönster

Vi tillåter en typ som ett mönster:

primary_pattern
    : type-pattern
    | // all of the existing forms
    ;
type_pattern
    : type
    ;

Detta retkonstruerar den befintliga is-type-expression till att vara en is-pattern-expression där mönstret är en type-pattern, även om vi inte skulle ändra syntaxträdet som skapas av kompilatorn.

Ett subtilt implementeringsproblem är att den här grammatiken är tvetydig. En sträng som a.b kan parsas antingen som ett kvalificerat namn (i en typkontext) eller ett prickat uttryck (i en uttryckskontext). Kompilatorn kan redan behandla ett kvalificerat namn på samma sätt som en punktnotation för att hantera något som liknar e is Color.Red. Kompilatorns semantiska analys skulle utökas ytterligare för att kunna binda ett (syntaktiskt) konstant mönster (t.ex. ett prickat uttryck) som en typ för att behandla den som ett mönster av bunden typ för att stödja den här konstruktionen.

Efter den här ändringen skulle du kunna skriva

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

Relationsmönster

Relationsmönster gör det möjligt för programmeraren att uttrycka att ett indatavärde måste uppfylla en relationsbegränsning jämfört med ett konstant värde:

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

Relationsmönster stöder relationsoperatorerna <, <=, >och >= på alla inbyggda typer som stöder sådana binära relationsoperatorer med två operander av samma typ i ett uttryck. Mer specifikt stöder vi alla dessa relationsmönster för sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, nintoch nuint.

primary_pattern
    : relational_pattern
    ;
relational_pattern
    : '<' relational_expression
    | '<=' relational_expression
    | '>' relational_expression
    | '>=' relational_expression
    ;

Uttrycket måste kunna utvärderas till ett konstant värde. Det är ett fel om det konstanta värdet är double.NaN eller float.NaN. Det är ett fel om uttrycket är en null-konstant.

När indata är en typ för vilken en lämplig inbyggd binär relationsoperator definieras som är tillämplig med indata som dess vänstra operande och den angivna konstanten som dess högra operande, tas utvärderingen av operatorn som innebörden av relationsmönstret. Annars konverterar vi indata till typen av uttryck med hjälp av en explicit nullbar konvertering eller avboxningskonvertering. Det är ett kompileringsfel om det inte finns någon sådan konvertering. Mönstret anses inte matcha om konverteringen misslyckas. Om konverteringen lyckas är resultatet av mönstermatchningsåtgärden resultatet av utvärderingen av uttrycket e OP v där e är de konverterade indata, OP är relationsoperatorn och v är det konstanta uttrycket.

Mönsterkombinatorer

Mönster kombinatorer tillåter matchning av båda de olika mönstren med hjälp av and (detta kan utökas till valfritt antal mönster genom upprepad användning av and), något av de två olika mönstren med hjälp av or (ditto), eller negeringen av ett mönster med not.

En vanlig användning av en kombinator kommer att vara idiomatiska uttryck.

if (e is not null) ...

Det här mönstret är mer läsbart än det aktuella formspråket e is objectoch visar tydligt att man söker efter ett värde som inte är null.

Kombinatorerna and och or är användbara för att testa värden

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Det här exemplet visar att and har en högre parsningsprioritet (dvs. kommer att binda närmare) än or. Programmeraren kan använda det parenteserade mönstret för att göra prioriteten explicit:

bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

Liksom alla mönster kan dessa kombinatorer användas i alla sammanhang där ett mönster förväntas, inklusive inbäddade mönster, is-pattern-expression, switch-expressionoch mönstret för en switch-satsens case-etikett.

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
    ;

Ändra till 6.2.5 Grammatik tvetydigheter

På grund av introduktionen av typmönstretär det möjligt att en allmän typ visas innan token =>. Därför lägger vi till => till den uppsättning token som anges i §6.2.5 Grammatikens tvetydigheter för att möjliggöra avtvetyda de < som påbörjar typargumentlistan. Se även https://github.com/dotnet/roslyn/issues/47614.

Öppna frågor med föreslagna ändringar

Syntax för relationsoperatorer

Är and, oroch not någon form av kontextuellt nyckelord? I så fall sker en brytande förändring (t.ex. jämfört med deras användning som designator i ett deklarationsmönster).

Semantik (t.ex. typ) för relationsoperatorer

Vi förväntar oss att stödja alla primitiva typer som kan jämföras i ett uttryck med hjälp av en relationsoperator. Innebörden i enkla fall är tydlig

bool IsValidPercentage(int x) => x is >= 0 and <= 100;

Men när indata inte är en sådan primitiv typ, vilken typ försöker vi konvertera den till?

bool IsValidPercentage(object x) => x is >= 0 and <= 100;

Vi har föreslagit att när indatatypen redan är en jämförbar primitiv är det typen av jämförelse. Men när indata inte är en jämförbar primitiv behandlar vi relationen som att inkludera ett implicit typtest till typen av konstanten på höger sida av relationen. Om programmeraren har för avsikt att stödja mer än en indatatyp måste detta göras uttryckligen:

bool IsValidPercentage(object x) => x is
    >= 0 and <= 100 or    // integer tests
    >= 0F and <= 100F or  // float tests
    >= 0D and <= 100D;    // double tests

Resultat: Relationsdelen innehåller ett implicit typtest för typen av konstant till höger om relationsdelen.

Flödande typinformation från vänster till höger om and

Det har föreslagits att när du skriver en and-kombinator kan typesinformation som lärts på vänster sida om den översta nivån flöda till höger. Till exempel

bool isSmallByte(object o) => o is byte and < 100;

Här begränsas den indatatypen till det andra mönstret av typ som begränsar krav till vänster om and. Vi definierar typförsnävningssemantik för alla mönster enligt följande. Den begränsade typen av ett mönster P definieras på följande sätt:

  1. Om P är ett typmönster är den begränsade typen typen av typmönstret.
  2. Om P är ett deklarationsmönster är den begränsade typen typen av deklarationsmönster.
  3. Om P är ett rekursivt mönster som ger en explicit typ är den begränsade typen den typen.
  4. Om Pmatchas via reglerna förITupleär den begränsade typen typen System.Runtime.CompilerServices.ITuple.
  5. Om P är ett konstant mönster där konstanten inte är null-konstanten och uttrycket inte har någon konvertering av konstanta uttryck till indatatyp, är den begränsade typen typen av konstant.
  6. Om P är ett relationsmönster där konstantuttrycket inte har någon konvertering av konstantuttryck till indatatypen , är den begränsade typen av konstanten.
  7. Om P är ett or-mönster, är den -begränsade typen den gemensamma typen för de -begränsade typerna av delmönstren, om en sådan gemensam typ finns. För detta ändamål tar den gemensamma typalgoritmen endast hänsyn till identitets-, boxnings- och implicita referenskonverteringar, och den tar hänsyn till alla undermönster i en sekvens med or mönster (ignorerar parenteserade mönster).
  8. Om P är ett and-mönster är den begränsade typen den begränsade typen av det rätta mönstret. Dessutom är den snäva typen av det vänstra mönstret den indatatypen för det högra mönstret.
  9. Annars är den begränsade typen av PPindatatyp.

Resultat: Ovanstående smalare semantik har implementerats.

Variabeldefinitioner och bestämd tilldelning

Tillägget av or och not mönster skapar några intressanta nya problem kring mönstervariabler och bestämd tilldelning. Eftersom variabler normalt kan deklareras högst en gång verkar det som om alla mönstervariabler som deklareras på ena sidan av ett or mönster inte definitivt skulle tilldelas när mönstret matchar. På samma sätt skulle en variabel som deklarerats i ett not mönster inte förväntas tilldelas definitivt när mönstret matchar. Det enklaste sättet att åtgärda detta är att förbjuda deklarering av mönstervariabler i dessa kontexter. Detta kan dock vara för restriktivt. Det finns andra metoder att tänka på.

Ett scenario som är värt att överväga är detta

if (e is not int i) return;
M(i); // is i definitely assigned here?

Detta fungerar inte idag eftersom, för ett is-pattern-expression, mönstervariablerna anses vara definitivt tilldelade endast där is-pattern-expression är sant ("definitivt tilldelade när det är sant").

Att stödja detta skulle vara enklare (ur programmerarens perspektiv) än att också lägga till stöd för ett if uttalande med negerade villkor. Även om vi lägger till sådant stöd skulle programmerare undra varför kodfragmentet ovan inte fungerar. Å andra sidan är samma scenario i en switch mindre meningsfullt, eftersom det inte finns någon motsvarande punkt i programmet där definitivt tilldelas när falska skulle vara meningsfulla. Skulle vi tillåta detta i en is-pattern-expression men inte i andra sammanhang där mönster tillåts? Det verkar oregelbundet.

Relaterat till detta är problemet med bestämd tilldelning i en disjunctive-pattern.

if (e is 0 or int i)
{
    M(i); // is i definitely assigned here?
}

Vi förväntar oss bara att i definitivt tilldelas när indata inte är noll. Men eftersom vi inte vet om indata är noll eller inte i blocket, i är inte definitivt tilldelad. Men vad händer om vi tillåter att i deklareras i olika ömsesidigt uteslutande mönster?

if ((e1, e2) is (0, int i) or (int i, 0))
{
    M(i);
}

Här tilldelas variabeln i definitivt inuti blocket och tar sitt värde från det andra elementet i tuplet när man hittar ett nollelement.

Det har också föreslagits att variabler ska (multipelt) definieras i varje fall av ett case-block.

    case (0, int x):
    case (int x, 0):
        Console.WriteLine(x);

För att göra något av detta arbete måste vi noggrant definiera var sådana flera definitioner tillåts och under vilka villkor en sådan variabel anses definitivt tilldelad.

Bör vi välja att skjuta upp sådant arbete till senare (vilket jag rekommenderar), kan vi säga i C# 9

  • under en not eller orkan det hända att mönstervariabler inte deklareras.

Sedan skulle vi ha tid att utveckla vissa erfarenheter som skulle ge insikt i det möjliga värdet av att koppla av det senare.

Resultat: Mönstervariabler kan inte deklareras under ett not eller or mönster.

Diagnostik, subsumtion och uttömmande

Dessa nya mönsterformulär ger många nya möjligheter att diagnostisera programmerfel. Vi måste bestämma vilka typer av fel vi ska diagnostisera och hur vi ska göra det. Här följer några exempel:

case >= 0 and <= 100D:

Det här fallet kan aldrig matcha (eftersom inmatningen inte kan vara både en int och en double). Vi identifierar redan ett problem när vi hittar ett fall som aldrig kan stämma överens, men dess formulering ("Växelfallet har redan hanterats av ett tidigare fall" och "Mönstret har redan hanterats av en tidigare arm i switch-uttrycket") kan leda till missförstånd i nya scenarier. Vi kan behöva ändra formuleringen för att bara säga att mönstret aldrig matchar indata.

case 1 and 2:

På samma sätt skulle detta vara ett fel eftersom ett värde inte kan vara både 1 och 2.

case 1 or 2 or 3 or 1:

Det här fallet är möjligt att matcha, men or 1 i slutet lägger ingen mening till mönstret. Jag föreslår att vi strävar efter att framkalla ett fel när någon konjunktion eller disjunkt av ett sammansatt mönster varken definierar en mönstervariabel eller påverkar uppsättningen av matchade värden.

case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;

Här lägger 0 or 1 or inte till något i det andra fallet, eftersom dessa värden skulle ha hanterats av det första fallet. Detta är också fel.

byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };

Ett switch-uttryck som detta bör betraktas som uttömmande (det hanterar alla möjliga indatavärden).

I C# 8.0 anses ett växeluttryck med indata av typen byte endast vara uttömmande om det innehåller en sista arm vars mönster matchar allt (ett ignorera-mönster eller var-mönster). Inte ens ett switch-uttryck som har en arm för varje distinkt byte värde anses vara uttömmande i C# 8. För att kunna hantera relationsmönstrens fullständighet korrekt måste vi också hantera det här fallet. Detta kommer tekniskt sett att vara en brytande förändring, men troligtvis kommer ingen användare att märka det.