Gelabeld break en continue instructies

Note

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.

Overzicht

Toestaan break en continue instructies om desgewenst een label op te geven waarmee wordt aangegeven welke lus of switch instructie moet worden gericht, waardoor een schonere controlestroom in geneste constructies mogelijk is zonder dat goto instructies of andere afpersingen, zoals geneste functies, tuple-retourneert, enzovoort.

Motivatie

Wanneer ontwikkelaars werken met geneste lussen of lussen die instructies bevatten switch , moeten ontwikkelaars vaak een buitenste lus breken of voortzetten vanuit een binnenste context. Er zijn momenteel twee primaire benaderingen om dit te bereiken, beide met aanzienlijke nadelen:

Instructies gebruiken goto

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

Hoewel goto het werkt, moeten labels na de lusconstructie worden geplaatst en wordt de intentie niet duidelijk gecommuniceerd om van een specifieke lus te breken. Voor het voortzetten van een buitenste lus wordt de benadering nog moeilijker:

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

Dit patroon is verwarrend omdat het label aan het einde van de lustekst moet worden geplaatst, net vóór de accolade sluiten, zodat de incrementor- en voorwaardecontrole plaatsvindt. Wanneer beide break en continue nodig zijn voor dezelfde buitenste lus, zijn twee afzonderlijke labels vereist:

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

Vlagvariabelen gebruiken

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

Voor deze aanpak is extra statusbeheer vereist, wordt de code uitgebreider en wordt de intentie van de controlestroom verborgen.

Voorgestelde oplossing

Met gelabeld break en continuewordt de code duidelijker en beter onderhoudbaar:

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

Het label wordt rechtstreeks op de lus geplaatst die wordt geïdentificeerd en de instructie break/continue noemt het doel expliciet. Als u doorgaat, gaat u als volgende te werk

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

Dit geeft natuurlijk "continue the outer loop", zonder de verwarring van labelplaatsing die is gekoppeld aan goto. Voor beide bewerkingen kan één label worden gebruikt:

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

Deze functie is uitgebreid aangevraagd in de C#-community, met discussies die terug decennia duren en het onderwerp dat voortdurend opnieuw wordt geïntroduceerd en opnieuw wordt aangevraagd. Vergelijkbare functies bestaan in verschillende andere moderne talen:

In al deze gevallen werken de talen op dezelfde manier als in deze specificatie. Sommige constructies kunnen namelijk een label hebben en het is mogelijk om naar dat label te verwijzen vanuit hun respectieve continue of break instructies.

Gedetailleerd ontwerp

De volgende updates worden weergegeven als een diff ten opzichte van de bijbehorende secties van de C# 7-standaard (statements.md). In deze sectie geeft doorhalen aan dat tekst wordt verwijderd uit de bestaande specificatie en vetgedrukt geeft aan dat tekst wordt toegevoegd. Ongewijzigde proza wordt letterlijke bewoordingen voor context citeren.

§13.5 Gelabelde instructies

Voeg de volgende alinea in direct na de bestaande alinea "Er kan naar een label worden verwezen vanuit goto instructies (§13.10.4) binnen het bereik van het label.':

Als de instructie onmiddellijk genest is binnen een labeled_statement een switch_statement (§13.8.3) of een iteration_statement (§13.9), wordt de geneste instructie aangeduid met de id van de labeled_statement. Een break_statement (§13.10.2) of continue_statement (§13.10.3) kan een dergelijke id opgeven om naar de gelabelde instructie te verwijzen.

Opmerking: Alleen de instructie die direct is genest binnen een labeled_statement is gelabeld met die id. Bijvoorbeeld, geeft a: b: while (…) …b alleen de iteration_statement labelt; a de binnenste labeled_statementb: while (…) …, die zelf geen switch_statement of iteration_statement is. Als gevolg hiervan break a; of continue a; het verschijnen in de hoofdtekst van de lus is niet gericht op de while instructie. eindnotitie

§13.10.2 De onderbrekingsinstructie

break_statement
    : 'break' identifier? ';'
    ;

De break instructie sluit de dichtstbijzijnde omsluitingswitch, while, doof forforeach instructie.

De break instructie sluit de dichtstbijzijnde switch_statement (§13.8.3) of iteration_statement (§13.9) of, als er een id is opgegeven, de dichtstbijzijnde switch_statement of iteration_statement gelabeld met die id (zie §13.5).

Het doel van een break instructie is het eindpunt van de dichtstbijzijnde insluitings switch-, while, do, - forof instructie- of foreach instructiedie hierboven is bepaald. Als een break instructie niet is ingesloten door een switch, while, doof forforeach instructie, treedt er een compilatietijdfout op.Als er geen dergelijke instructie bestaat, treedt er een compilatietijdfout op.

Wanneer meerdere switch, while, do, of forforeach instructies zijn genest binnen elkaar, is een break instructie alleen van toepassing op de binnenste instructie. Om controle over meerdere nestniveaus over te dragen, wordt een goto instructie (§13.10.4) gebruikt.

Een break instructie kan geen finally blok afsluiten (§13.11). Wanneer een break instructie binnen een finally blok plaatsvindt, bevindt het doel van de break instructie zich binnen hetzelfde finally blok; anders treedt er een compilatiefout op.

Er wordt als volgt een break instructie uitgevoerd:

  • Als de break instructie een of meer try blokken met gekoppelde finally blokken afsluit, wordt het besturingselement in eerste instantie overgebracht naar het finally blok van de binnenste try instructie. Wanneer en als het besturingselement het eindpunt van een finally blok bereikt, wordt het besturingselement overgebracht naar het blok van de finally volgende insluitingsinstructie try . Dit proces wordt herhaald totdat de finally blokken van alle tussenliggende try instructies zijn uitgevoerd.
  • Het besturingselement wordt overgebracht naar het doel van de break instructie.

Omdat een break verklaring voorwaardelijke controle elders overdraagt, is het eindpunt van een break instructie nooit bereikbaar.

Voorbeeld: Een gelabelde break oplossing wordt omgezet in de dichtstbijzijnde switch_statement of iteration_statement met het overeenkomende label:

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

eindvoorbeeld

§13.10.3 De doorzettingsverklaring

continue_statement
    : 'continue' identifier? ';'
    ;

De continue instructie start een nieuwe iteratie van de dichtstbijzijnde insluitingwhile, doof forforeach instructie.

Met continue de instructie wordt een nieuwe iteratie gestart van de dichtstbijzijnde iteration_statement (§13.9) of, als er een id is opgegeven, de dichtstbijzijnde iteration_statement gelabeld met die id (zie §13.5).

Het doel van een continue instructie is het eindpunt van de ingesloten instructie van de dichtstbijzijnde omsluitendewhile, doforof foreachinstructie iteration_statement zoals hierboven is bepaald. Als een continue instructie niet is ingesloten door een while, doof forforeach instructie, treedt er een compilatietijdfout op.Als er geen dergelijke instructie bestaat, treedt er een compilatietijdfout op.

Wanneer meerdere while, doof forforeach instructies binnen elkaar zijn genest, is een continue instructie alleen van toepassing op de binnenste instructie. Om controle over meerdere nestniveaus over te dragen, wordt een goto instructie (§13.10.4) gebruikt.

Een continue instructie kan geen finally blok afsluiten (§13.11). Wanneer een continue instructie binnen een finally blok plaatsvindt, bevindt het doel van de continue instructie zich binnen hetzelfde finally blok; anders treedt er een compilatiefout op.

Er wordt als volgt een continue instructie uitgevoerd:

  • Als de continue instructie een of meer try blokken met gekoppelde finally blokken afsluit, wordt het besturingselement in eerste instantie overgebracht naar het finally blok van de binnenste try instructie. Wanneer en als het besturingselement het eindpunt van een finally blok bereikt, wordt het besturingselement overgebracht naar het blok van de finally volgende insluitingsinstructie try . Dit proces wordt herhaald totdat de finally blokken van alle tussenliggende try instructies zijn uitgevoerd.
  • Het besturingselement wordt overgebracht naar het doel van de continue instructie.

Omdat een continue verklaring voorwaardelijke controle elders overdraagt, is het eindpunt van een continue instructie nooit bereikbaar.

Voorbeeld: Een gelabelde continue oplossing wordt omgezet in het dichtstbijzijnde iteration_statement met het overeenkomende label:

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

eindvoorbeeld

Nadelen/alternatieven

Instructies blijven gebruiken goto

C# biedt al ondersteuning gotovoor dezelfde controlestroom. goto Heeft echter verschillende nadelen ten opzichte van gelabelde onderbreking/continue:

  • Vereist afzonderlijke labels voor onderbreking versus continue scenario's (onderbrekingslabels gaan na de lus, doorgaan met labels gaan voor de accolade sluiten)
  • Labelplaatsing is minder intuïtief en verschilt afhankelijk van het feit of u breekt of doorgaat
  • Minder expliciet over intenties (springen naar een locatie versus het verbreken/voortzetten van een specifieke lus)
  • Broos en foutgevoelig: ontwikkelaars moeten ervoor zorgen dat er geen instructies per ongeluk tussen labels en hun doelconstructies worden geplaatst. Met gevolgd doorEND_LOOP:, goto END_LOOP; is het bijvoorbeeld eenvoudig om per ongeluk een instructie tussen deze instructies in te voegen tijdens het onderhoud, waardoor de beoogde controlestroom wordt verbroken. Gelabelde lussen voorkomen dit probleem door het label rechtstreeks aan de constructie te koppelen.
  • Draagt historische stigma die gelabelde break/continue vermijdt

Vlagvariabelen gebruiken

Zoals wordt weergegeven in de sectie Motivatie, werken vlagvariabelen, maar voeg belangrijke standaard toe en verdoezelt u de logica van de controlestroom.

Gebruik break N of continue N met numerieke niveaus

  • Breekbaar tijdens het herstructureren (het toevoegen/verwijderen van een lusniveau vereist het bijwerken van alle numerieke verwijzingen)
  • Moeilijker te lezen (moet niveaus tellen om het doel te begrijpen)
  • Minder expliciet dan benoemde labels
  • Gebrek aan duidelijkheid (op basis van 1? op basis van 0?)

Herstructureren in afzonderlijke methoden

Hoewel dit vaak een goede gewoonte is, is het niet altijd haalbaar of geschikt en introduceert soms onnodige complexiteit voor wat een eenvoudige controlestroom moet zijn.

In dit voorstel worden de volgende communitydiscussies geconsolideerd en aangepakt:

Open vragen

labelsemantiek

De huidige specificatie definieert de semantiek voor break identifier/continue identifier het vinden van de binnenste toepasselijke lus/switch-constructie met die id en verzendt vervolgens naar de break/continue semantiek. Een alternatieve formalisatie is om in plaats daarvan te zeggen dat break identifier/continue identifier een label wordt geïdentificeerd met dezelfde regels als 'goto'. En als het label rechtstreeks een lus/switch-constructie bevat die het einde/doorgaan omsluit, is dat de lus/switch waarop het einde/doorgaan van toepassing is.

Beide formalisaties zijn in feite identiek, waardoor dezelfde set programma's wordt toegestaan en de toewijzing ervan ongedaan wordt gemaakt. De benadering die in deze speclet is gekozen, is gedaan voor zowel conceptuele als letterlijke eenvoud. Het hoeft niet om het bereik van labels of de binding van id's te dekken, net zoals goto het, of om uitgaande bindingslogica te weigeren voor het omzetten van de lus/switch en continue/break-instructie. In plaats daarvan wordt de eenvoudige specificatietaal uitgebreid waarmee de juiste omsluitlus/switch wordt gevonden op basis van de onderbreking/continue, zodat deze voorbij de binnenste taal kan worden uitgebreid, naar iets erboven.

Als LDM deze strakker voelt om naar +label semantiek te gaan, zou het niet moeilijk zijn om de specificatie hierop aan te passen. Deze vraag open houden als de groep denkt dat de laatste vorm natuurlijker is dan de hier genomen vorm.

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

Specificatie van hoe labels worden gedeclareerd:

Elk blok of switch_block maakt een afzonderlijke declaratieruimte voor labels. Namen worden via labeled_statements in deze declaratieruimte ingevoerd en de namen worden via goto_statements verwezen. De labeldeclaratieruimte van een blok bevat geneste blokken. Daarom is het binnen een geneste blok niet mogelijk om een label met dezelfde naam als een label in een omsluitblok te declareren.

Specificatie van gelabelde instructies:

Het bereik van een label is het hele blok waarin het label wordt gedeclareerd, inclusief geneste blokken. Het is een compilatiefout voor twee labels met dezelfde naam om overlappende bereiken te hebben.
Er kan naar een label worden verwezen vanuit goto-instructies (§13.10.4) binnen het bereik van het label.

geneste labels

Moet a: b: while (true) continue a; worden ondersteund?

Aanbeveling: nee. Er zijn geen gebruikers om dit gevraagd. Er zijn geen aantrekkelijke use cases gepresenteerd. De meeste gangbare talen staan het niet toe, zonder klachten van hun community's. De lang (en impl) zijn eenvoudiger en duidelijker, brengen strikt toe dat alleen de directe met gelabelde instructielabels de lus/switch labelt.

Ontwerpbijeenkomsten

TBD