Delen via


Wijzigingen in patroonherkenning voor C# 9.0

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting).

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

We overwegen een klein aantal verbeteringen aan patroonkoppeling voor C# 9.0 die natuurlijke synergie hebben en goed werken om een aantal veelvoorkomende programmeerproblemen op te lossen:

Patronen met haakjes

Met geparentheseerde patronen kan de programmeur haakjes rond elk patroon plaatsen. Dit is niet zo nuttig met de bestaande patronen in C# 8.0, maar de nieuwe patrooncombinaties hebben een prioriteit die de programmeur mogelijk wil overschrijven.

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

Typepatronen

We maken een type als patroon toe:

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

Hiermee wordt de bestaande is-type-expressie een is-patroon-expressie waarin het patroon een type-patroonis, maar de door de compiler geproduceerde syntaxisstructuur wordt niet gewijzigd.

Een subtiel implementatieprobleem is dat deze grammatica dubbelzinnig is. Een tekenreeks zoals a.b kan worden geparseerd als een gekwalificeerde naam (in een typecontext) of een gestippelde expressie (in een expressiecontext). De compiler kan al een gekwalificeerde naam als een gestippelde expressie behandelen om iets als e is Color.Redte verwerken. De semantische analyse van de compiler zou verder worden uitgebreid om een (syntactisch) constant patroon (bijvoorbeeld een gestippelde expressie) te binden als een type om het als een afhankelijk typepatroon te behandelen om deze constructie te ondersteunen.

Na deze wijziging kunt u schrijven

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

Relationele patronen

Met relationele patronen kan de programmeur uitdrukken dat een invoerwaarde moet voldoen aan een relationele beperking in vergelijking met een constante waarde:

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

Relationele patronen ondersteunen de relationele operators <, <=, >en >= op alle ingebouwde typen die ondersteuning bieden voor dergelijke binaire relationele operators met twee operanden van hetzelfde type in een expressie. We ondersteunen met name al deze relationele patronen voor sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, ninten nuint.

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

De expressie is vereist om een constante waarde te evalueren. Het is een fout als die constante waarde is double.NaN of float.NaN. Dit is een fout als de expressie een null-constante is.

Wanneer de invoer een type is waarvoor een geschikte ingebouwde binaire relationele operator is gedefinieerd die van toepassing is op de invoer als linkeroperand en de opgegeven constante als de rechteroperand, wordt de evaluatie van die operator beschouwd als de betekenis van het relationele patroon. Anders zetten we de invoer om naar het type van de expressie met behulp van een expliciete nullable of unboxing conversie. Het is een compilatiefout als er geen conversie bestaat. Het patroon wordt beschouwd als niet overeenkomend als de conversie mislukt. Als de conversie slaagt, is het resultaat van de patroonkoppelingsbewerking het resultaat van het evalueren van de expressie e OP v waar e de geconverteerde invoer is, OP de relationele operator is en v de constante expressie is.

Patrooncombinaties

Patroon combinatoren maken het mogelijk om beide verschillende patronen te matchen met behulp van and (dit kan worden uitgebreid tot een willekeurig aantal patronen door het herhaalde gebruik van and), een van de twee verschillende patronen te gebruiken met behulp van or (idem), of de negatie van een patroon met behulp van not.

Een gemeenschappelijk gebruik van een combinator is het idioom

if (e is not null) ...

Beter leesbaar dan het huidige idioom e is object, geeft dit patroon duidelijk aan dat er wordt gecontroleerd op een niet-null-waarde.

De and en or combinaties zijn handig voor het testen van waardenbereiken

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

In dit voorbeeld ziet u dat and een hogere parseringsprioriteit (d.w.v. meer bindingen) heeft dan or. De programmeur kan het haakjes patroon gebruiken om de prioriteit expliciet te maken:

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

Net als alle patronen kunnen deze combinatoren worden gebruikt in elke context waarin een patroon wordt verwacht, inclusief geneste patronen, de is-pattern-expressie, de switch-expressieen het patroon van het case-label van een switch-instructie.

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
    ;

Wijzigen in 6.2.5 Grammatica ambiguïteiten

Vanwege de introductie van het typepatroon, is het mogelijk dat een algemeen type wordt weergegeven vóór het token =>. Daarom voegen we => toe aan de set tokens die worden vermeld in §6.2.5 Grammatica-dubbelzinnigheden om ondubbelzinnigheid toe te staan van de < die begint met de lijst met typeargumenten. Zie ook https://github.com/dotnet/roslyn/issues/47614.

Openstaande problemen met voorgestelde wijzigingen

Syntaxis voor relationele operatoren

Zijn and, oren not een contextueel trefwoord? Als dat het geval is, is er dan sprake van een breaking change (bijvoorbeeld in vergelijking met hun gebruik als aanduiding in een declaratie-patroon).

Semantiek (bijvoorbeeld type) voor relationele operators

We verwachten dat we alle primitieve typen ondersteunen die kunnen worden vergeleken in een expressie met behulp van een relationele operator. De betekenis in eenvoudige gevallen is duidelijk

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

Maar als de invoer niet zo'n primitief type is, naar welk type proberen we deze te converteren?

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

We hebben voorgesteld dat wanneer het invoertype al een vergelijkbaar primitieve type is, dat het het type is waarmee wordt vergeleken. Als de invoer echter geen vergelijkbare primitieve is, behandelen we de relatie als met een impliciete typetest voor het type van de constante aan de rechterkant van de relatie. Als de programmeur meer dan één invoertype wil ondersteunen, moet dit expliciet worden gedaan:

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

Resultaat: De relationele uitdrukking bevat een impliciete typetest voor het type van de constante rechts van de uitdrukking.

Stromende type-informatie van links naar rechts van and

Het is voorgesteld dat wanneer u een and combinator schrijft, type-informatie die links is geleerd over het toplevelt type, naar rechts kan stromen. Bijvoorbeeld

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

Hier wordt het invoertype tot het tweede patroon beperkt door het type dat vereisten van links van het andbeperkt. We definiëren de vernauwende type-semantiek voor alle patronen als volgt. Het beperkte type van een patroon P wordt als volgt gedefinieerd:

  1. Als P een typepatroon is, is het beperkte type het type van het type patroon.
  2. Als P een declaratiepatroon is, is het beperkte type het type van het declaratiepatroon.
  3. Als P een recursief patroon is dat een expliciet type geeft, is het beperkte type dat type.
  4. Als Povereenkomen met de regels voorITuple, is het beperkte type het type System.Runtime.CompilerServices.ITuple.
  5. Als P een constant patroon is waarbij de constante niet de null-constante is en waarbij de expressie geen constante expressieconversie heeft naar het invoertype, is het beperkte type het type van de constante.
  6. Als P een relationeel patroon is waarbij de constante expressie geen constante expressieconversie naar het invoertype, is het beperkt type het type van de constante.
  7. Als P een or patroon is, is het verkleind type het gemeenschappelijke type van het verkleind type van de subpatronen als een dergelijk gemeenschappelijk type bestaat. Voor dit doel beschouwt het gebruikelijke type-algoritme alleen identiteit, boxing en impliciete verwijzingsconversies, en wordt rekening gehouden met alle subpatronen van een reeks or-patronen, waarbij tussen haakjes staande patronen worden genegeerd.
  8. Als P een and-patroon is, is het beperkte type hetzelfde als het beperkte type van het rechterpatroon. Bovendien is het beperkte type van het linkerpatroon het invoertype van het rechterpatroon.
  9. Anders is het vernauwde type van van P het invoertype van P.

Resultaat: De hierboven vermelde verengde semantiek is geïmplementeerd.

Variabeledefinities en definitieve toewijzing

Door het toevoegen van or en not patronen ontstaan er interessante nieuwe problemen rond patroonvariabelen en definitieve toewijzing. Aangezien variabelen normaal gesproken maximaal één keer kunnen worden gedeclareerd, lijkt het alsof een patroonvariabele aan één kant van een or patroon niet zeker wordt toegewezen wanneer het patroon overeenkomt. Op dezelfde manier wordt van een variabele die in een not-patroon is gedeclareerd niet verwacht dat deze een definitieve waarde toegewezen krijgt wanneer het patroon overeenkomt. De eenvoudigste manier om dit te verhelpen, is het verbieden van het declareren van patroonvariabelen in deze contexten. Dit kan echter te beperkend zijn. Er zijn andere benaderingen om te overwegen.

Een scenario dat de moeite waard is om rekening mee te houden, is dit

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

Dit werkt momenteel niet omdat voor een is-pattern-expression, de patroonvariabelen worden beschouwd als zeker toegewezen alleen als de is-pattern-expression waar is ('zeker toegewezen als waar').

Dit zou eenvoudiger zijn (vanuit het perspectief van de programmeur) dan ook ondersteuning toe te voegen voor een if-instructie bij een omgekeerde voorwaarde. Zelfs als we dergelijke ondersteuning toevoegen, vragen programmeurs zich af waarom het bovenstaande fragment niet werkt. Aan de andere kant is hetzelfde scenario in een switch minder zinvol, omdat er geen corresponderend punt in het programma is waar zeker toegewezen wanneer onwaar zinvol zou zijn. Zouden we dit toestaan in een is-pattern-expression, maar niet in andere contexten waar patronen zijn toegestaan? Dat lijkt onregelmatig.

Gerelateerd hieraan is het probleem van een definiete toewijzing in een disjunctief patroon.

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

We verwachten alleen dat i zeker wordt toegewezen wanneer de invoer niet nul is. Maar omdat we niet weten of de invoer nul is of niet binnen het blok, is i niet zeker toegewezen. Wat gebeurt er echter als we toestaan dat i in verschillende wederzijds exclusieve patronen worden gedeclareerd?

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

Hier wordt de variabele i eenduidig in het blok toegewezen en ontvangt zijn waarde van het andere element van de tuple wanneer er een nulcomponent wordt gevonden.

Het is ook voorgesteld om toe te staan dat variabelen in elk geval van een case-blok meerdere keren kunnen worden gedefinieerd.

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

Om een van deze werkzaamheden te maken, moeten we zorgvuldig definiëren waar dergelijke meerdere definities zijn toegestaan en onder welke omstandigheden een dergelijke variabele wordt beschouwd als definitief toegewezen.

Mochten we ervoor kiezen om dergelijke werkzaamheden tot later uit te stellen (wat ik adviseer), zouden we in C# 9 kunnen zeggen dat...

  • onder een not of orworden patroonvariabelen mogelijk niet gedeclareerd.

Dan zouden we tijd hebben om wat ervaring op te doen die inzicht zou geven in de mogelijke waarde van het eventueel later los te laten.

Resultaat: patroonvariabelen kunnen niet worden gedeclareerd onder een not of or patroon.

Diagnostische gegevens, subsumption en uitputtendheid

Deze nieuwe patroonformulieren introduceren veel nieuwe mogelijkheden voor diagnosebare programmeursfouten. We moeten bepalen welke soorten fouten we gaan diagnosticeren en hoe we dit moeten doen. Hier volgen enkele voorbeelden:

case >= 0 and <= 100D:

Dit geval kan nooit overeenkomen (omdat de invoer niet zowel een int als een doublekan zijn). Er is al een fout opgetreden bij het detecteren van een case die nooit kan overeenkomen, maar de formulering ervan ('De switchcase is al verwerkt door een vorige case' en 'Het patroon is al verwerkt door een vorige arm van de switch-expressie') kan misleidend zijn in nieuwe scenario's. Mogelijk moeten we de tekst wijzigen om alleen te zeggen dat het patroon nooit overeenkomt met de invoer.

case 1 and 2:

Op dezelfde manier zou dit een fout zijn omdat een waarde niet zowel 1 als 2kan zijn.

case 1 or 2 or 3 or 1:

Dit geval kan overeenkomen, maar de or 1 aan het einde voegt geen betekenis toe aan het patroon. Ik stel voor dat we moeten streven naar het veroorzaken van een fout wanneer een conjunct of disjunct van een samengesteld patroon geen patroonvariabele definieert of invloed heeft op de set overeenkomende waarden.

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

Hier voegt 0 or 1 or niets toe aan het tweede geval, omdat deze waarden door het eerste geval zouden zijn verwerkt. Dit verdient ook een foutmelding.

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

Een switchexpressie zoals deze moet worden beschouwd als volledige (deze verwerkt alle mogelijke invoerwaarden).

In C# 8.0 wordt een schakelexpressie met invoer van het type byte alleen als volledig beschouwd als deze een laatste arm bevat waarvan het patroon overeenkomt met alles (een weglaatpatroon of var-patroon). Zelfs een schakelexpressie met een arm voor elke afzonderlijke byte waarde wordt niet als volledig beschouwd in C# 8. Om de volledigheid van relationele patronen goed af te handelen, moeten we ook dit geval verwerken. Dit is technisch gezien een belangrijke wijziging, maar er is waarschijnlijk geen enkele gebruiker opgevallen.