Etiketter och breakcontinue instruktioner

Note

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ära dig mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Sammanfattning

Tillåt break och continue instruktioner om du vill ange en etikett som identifierar vilken loop eller switch -instruktion som ska riktas, vilket aktiverar renare kontrollflöde i kapslade konstruktioner utan att kräva goto instruktioner eller andra contortioner som kapslade funktioner, tupplar returnerar osv.

Motivation

När du arbetar med kapslade loopar eller loopar som innehåller switch instruktioner måste utvecklare ofta bryta sig ur eller fortsätta en yttre loop inifrån en inre kontext. För närvarande finns det två huvudsakliga metoder för att uppnå detta, båda med betydande nackdelar:

Använda goto instruktioner

string foundValue = null;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            goto FOUND;
    }
}
FOUND:
ProcessValue(foundValue);

Även om goto det fungerar kräver det att du placerar etiketter efter loopkonstruktionen och inte tydligt kommunicerar avsikten att bryta från en specifik loop. För att fortsätta en yttre loop blir metoden ännu mer besvärlig:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
    }
    CONTINUE_OUTER: ;
}

Det här mönstret är förvirrande eftersom etiketten måste placeras i slutet av looptexten, precis före den avslutande klammerparentesen, så att inkrementorn och villkorskontroll sker. När både break och continue behövs för samma yttre loop krävs två separata etiketter:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
        
        if (ShouldExitAll(x, y))
            goto BREAK_OUTER;
    }
    CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements

Använda flaggvariabler

string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
        {
            shouldBreak = true;
            break;
        }
    }
    if (shouldBreak)
        break;
}
ProcessValue(foundValue);

Den här metoden kräver ytterligare tillståndshantering, ökar kodverifieringen och döljer avsikten för kontrollflödet.

Föreslagen lösning

Med etiketten break och continueblir koden tydligare och mer underhållsbar:

string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            break outer;
    }
}
ProcessValue(foundValue);

Etiketten placeras direkt på den loop som den identifierar, och break/continue-instruktionen namnger uttryckligen målet. För att fortsätta:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
    }
}

Detta uttrycker naturligtvis "fortsätt den yttre slingan", utan förvirring av etikettplacering som är associerad med goto. En enskild etikett kan användas för båda åtgärderna:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
        
        if (ShouldExitAll(x, y))
            break outer;
    }
}

Den här funktionen har begärts mycket i C#-communityn, med diskussioner som går tillbaka årtionden och ämnet återinförs och efterfrågas kontinuerligt. Liknande funktioner finns på flera andra moderna språk:

I alla dessa fall fungerar språken på samma sätt som i den här specifikationen. Vissa konstruktioner kan nämligen ha en etikett, och det är möjligt att referera till etiketten från respektive continue eller break -instruktioner.

Detaljerad design

Följande uppdateringar visas som ett diff mot motsvarande avsnitt i C# 7-standarden (statements.md). I det här avsnittet anger genomstrykning att text tas bort från den befintliga specifikationen, och fetstil anger att text läggs till. Oförändrad prosa citeras ordagrant för kontext.

§13.5 Etiketterade instruktioner

Infoga följande stycke omedelbart efter det befintliga stycket "En etikett kan refereras från goto instruktioner (§13.10.4) inom etikettens omfång.":

Om instruktionen omedelbart kapslas inom en labeled_statement är en switch_statement (§13.8.3) eller en iteration_statement (§13.9), sägs den kapslade instruktionen vara märkt medidentifieraren för labeled_statement. En break_statement (§13.10.2) eller continue_statement (§13.10.3) kan ange en sådan identifierare för att referera till den innehållande märkta instruktionen.

Obs!Endast instruktionen som omedelbart kapslas inom en labeled_statement är märkt med den identifieraren. Till exempel, givet a: b: while (…) …, etiketterar endast biteration_statement; a etiketterar den inre labeled_statementb: while (…) …, vilket inte i sig är en switch_statement eller iteration_statement. Det innebär att break a; instruktionen inte riktas mot eller continue a; visas i looptexten while . slutkommentar

§13.10.2 Brytsatsen

break_statement
    : 'break' identifier? ';'
    ;

-instruktionen break avslutar den närmaste omslutande switch, while, do, foreller foreach -instruktionen.

Instruktionen break avslutar närmaste omslutande switch_statement (§13.8.3) eller iteration_statement (§13.9), eller, om en identifierare anges, närmaste omslutande switch_statement eller iteration_statement märkt med den identifieraren (se §13.5).

Målet för en break -instruktion är slutpunkten för den närmaste omslutande switchinstruktionen , , whiledo, foreller foreach -instruktionensom bestäms enligt ovan. Om en break instruktion inte omges av en switchinstruktion , while, do, foreller foreach uppstår ett kompileringsfel.Om det inte finns någon sådan omslutande instruktion uppstår ett kompileringsfel.

När flera switch, while, do, foreller foreach -instruktioner kapslas inom varandra, gäller en break instruktion endast för den innersta instruktionen. För att överföra kontrollen över flera kapslingsnivåer ska en goto instruktion (§13.10.4) användas.

En break instruktion kan inte avsluta ett finally block (§13.11). När en break instruktion inträffar inom ett finally block ska målet för instruktionen break ligga inom samma finally block. Annars uppstår ett kompileringsfel.

En break instruktion körs på följande sätt:

  • Om instruktionen break avslutar ett eller flera try block med associerade finally block överförs finally kontrollen till blocket i den innersta try instruktionen. När och om kontrollen når slutpunkten för ett finally block överförs kontrollen till finally blocket för nästa omslutande-instruktion try . Den här processen upprepas tills blocken för finally alla mellanliggande try instruktioner har körts.
  • Kontrollen överförs till målet för -instruktionen break .

Eftersom en break instruktion villkorslöst överför kontrollen någon annanstans kan slutpunkten för en break -instruktion aldrig nås.

Exempel: En etikett break matchas till närmaste omslutande switch_statement eller iteration_statement med matchande etikett:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i * j > 20)
            break outer; // exits the outer for-loop
    }
}

slutexempel

§13.10.3 Fortsättningsinstruk-

continue_statement
    : 'continue' identifier? ';'
    ;

-instruktionen continue startar en ny iteration av närmaste omslutande while, do, foreller foreach -instruktion.

Instruktionen continue startar en ny iteration av närmaste omslutande iteration_statement (§13.9), eller, om en identifierare anges, närmaste omslutande iteration_statement märkt med identifieraren (se §13.5).

Målet för en continue -instruktion är slutpunkten för den inbäddade -instruktionen för närmaste omslutande while, do, foreller foreach -instruktioniteration_statement bestäms enligt ovan. Om en continue instruktion inte omges av en while- - doforeller foreach -instruktion uppstår ett kompileringsfel.Om det inte finns någon sådan omslutande instruktion uppstår ett kompileringsfel.

När flera while, do, foreller foreach -instruktioner kapslas inom varandra, gäller en continue -instruktion endast för den innersta instruktionen. För att överföra kontrollen över flera kapslingsnivåer ska en goto instruktion (§13.10.4) användas.

En continue instruktion kan inte avsluta ett finally block (§13.11). När en continue instruktion inträffar inom ett finally block ska målet för instruktionen continue ligga inom samma finally block. Annars uppstår ett kompileringsfel.

En continue instruktion körs på följande sätt:

  • Om instruktionen continue avslutar ett eller flera try block med associerade finally block överförs finally kontrollen till blocket i den innersta try instruktionen. När och om kontrollen når slutpunkten för ett finally block överförs kontrollen till finally blocket för nästa omslutande-instruktion try . Den här processen upprepas tills blocken för finally alla mellanliggande try instruktioner har körts.
  • Kontrollen överförs till målet för -instruktionen continue .

Eftersom en continue instruktion villkorslöst överför kontrollen någon annanstans kan slutpunkten för en continue -instruktion aldrig nås.

Exempel: En etikett continue matchas till närmaste omslutande iteration_statement med matchande etikett:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (ShouldSkip(i, j))
            continue outer; // continues the outer for-loop
    }
}

slutexempel

Nackdelar/alternativ

Fortsätt använda instruktioner goto

C# stöder gotoredan , vilket kan åstadkomma samma kontrollflöde. Har dock goto flera nackdelar jämfört med märkt break/continue:

  • Kräver separata etiketter för break vs. continue-scenarier (brytetiketter går efter loopen, fortsätt etiketterna går före den avslutande klammerparentesen)
  • Etikettplacering är mindre intuitivt och skiljer sig beroende på om du bryter eller fortsätter
  • Mindre explicit om avsikt (hoppa till en plats jämfört med att bryta/fortsätta en specifik loop)
  • Spröd och felbenägen: utvecklare måste se till att inga instruktioner oavsiktligt placeras mellan etiketter och deras målkonstruktioner. Med följt av END_LOOP:är det till exempel goto END_LOOP; enkelt att oavsiktligt infoga en instruktion mellan dem under underhåll, vilket bryter det avsedda kontrollflödet. Etiketterade loopar förhindrar det här problemet genom att binda etiketten direkt till konstruktionen.
  • Bär historiskt stigma som märkt break/continue undviker

Använda flaggvariabler

Som visas i motivationsavsnittet fungerar flaggvariabler men lägger till betydande pannplåt och döljer kontrollflödeslogik.

Använda break N eller continue N med numeriska nivåer

  • Bräcklig under refaktorisering (om du lägger till/tar bort en loopnivå måste alla numeriska referenser uppdateras)
  • Svårare att läsa (måste räkna nivåer för att förstå målet)
  • Mindre explicit än namngivna etiketter
  • Brist på klarhet (1-baserad? 0-baserad?)

Omstrukturera till separata metoder

Även om detta ofta är bra är det inte alltid möjligt eller lämpligt, och ibland införs onödig komplexitet för vad som ska vara ett enkelt kontrollflöde.

Det här förslaget konsoliderar och tar upp följande communitydiskussioner:

Öppna frågor

etikettsemantik

Den aktuella specifikationen definierar semantiken för break identifier/continue identifier som att hitta den innersta tillämpliga loop-/växelkonstruktionen märkt med den identifieraren och sedan skicka till den med standardsemantik.break/continue En alternativ formalisering är att i stället säga att break identifier/continue identifier identifierar en etikett med samma regler som "goto". Och om etiketten direkt innehåller en loop/växelkonstruktion som omsluter bryt/fortsätt, så är det den loop/växel som bryt/fortsätt gäller för.

Båda formaliseringarna är i praktiken identiska, vilket tillåter och tillåter inte samma uppsättning program. Metoden som valdes i den här specifikationen gjordes för både konceptuell och literal enkelhet. Den behöver inte täcka omfånget av etiketter, eller bindningen av identifierare som goto den gör, eller behöva definiera inåtgående bindningslogik för att matcha loopen/växeln och fortsätt/bryta-instruktionen. I stället expanderar det helt enkelt det enkla specifikationsspråket som hittar rätt omslutande loop/växel med tanke på pausen/fortsätt, vilket gör att det kan sträcka sig förbi det innersta, till något ovanför det.

Om LDM känner att binda detta snävare till goto + etikett semantik, skulle det inte vara svårt att justera specifikationen till det. Att hålla denna fråga öppen om gruppen anser att den senare formen är mer naturlig än den form som tas här.

void M()
{
  label:
  Console.WriteLine();
  
  foreach (var x in ...)
  {
    break label;
    // should this scenario fail because:
    // 1. the identifier lookup fails, or
    // 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
  }
}

Information om hur etiketter deklareras:

Varje block eller switch_block skapar ett separat deklarationsutrymme för etiketter. Namn introduceras i det här deklarationsutrymmet via labeled_statements och namnen refereras via goto_statements. Etikettdeklarationsutrymmet för ett block innehåller alla kapslade block. Inom ett kapslat block går det därför inte att deklarera en etikett med samma namn som en etikett i ett omslutande block.

Specifikation för etiketterade instruktioner:

Omfånget för en etikett är hela blocket där etiketten deklareras, inklusive kapslade block. Det är ett kompileringsfel för två etiketter med samma namn att ha överlappande omfång.
En etikett kan refereras från goto-instruktioner (§13.10.4) inom etikettens omfång.

kapslade etiketter

Bör a: b: while (true) continue a; stödjas?

Rekommendation: nej. Inga användare har bett om detta. Inga övertygande användningsfall har presenterats för det. De flesta vanliga språk tillåter det inte, utan klagomål från deras samhällen. Langen (och impl) är enklare och tydligare bringa strikt att endast den direkt som innehåller etiketterade instruktion etiketter slingan / växeln.

Designa möten

TBD