break Etykiety i continue instrukcje

Note

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Podsumowanie

Zezwalaj i breakcontinue na opcjonalne określanie etykiety, która identyfikuje pętlę lub switch instrukcję docelową, umożliwiając czystszy przepływ sterowania w konstrukcjach zagnieżdżonych bez wymagania goto instrukcji lub innych kontorcji, takich jak zagnieżdżone funkcje, zwraca krotki itp.

Motywacja

Podczas pracy z zagnieżdżonymi pętlami lub pętlami zawierającymi switch instrukcje deweloperzy często muszą przerwać lub kontynuować pętlę zewnętrzną z poziomu kontekstu wewnętrznego. Obecnie istnieją dwa podstawowe podejścia do osiągnięcia tego celu, oba z istotnymi wadami:

Używanie goto instrukcji

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

Chociaż goto działa, wymaga umieszczania etykiet po konstrukcji pętli i nie wyraźnie komunikuje zamiaru przerwania z określonej pętli. W przypadku kontynuowania pętli zewnętrznej podejście staje się jeszcze bardziej niezręczne:

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

Ten wzorzec jest mylący, ponieważ etykieta musi zostać umieszczona na końcu treści pętli, tuż przed zamykającym nawiasem klamrowym, aby można było sprawdzić przyrost i warunek. Jeśli obie break te pętli i continue są potrzebne w tej samej pętli zewnętrznej, wymagane są dwie oddzielne etykiety:

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

Używanie zmiennych flagi

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

Takie podejście wymaga dodatkowego zarządzania stanem, zwiększa szczegółowość kodu i ukrywa intencję przepływu sterowania.

Proponowane rozwiązanie

Z etykietami break i continuekod staje się jaśniejszy i bardziej konserwowalny:

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

Etykieta jest umieszczana bezpośrednio w pętli, z którym się identyfikuje, a instrukcja break/continue jawnie nazywa jej element docelowy. Aby kontynuować:

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

To naturalnie wyraża "kontynuację pętli zewnętrznej", bez pomyłek umieszczania etykiet skojarzonych z goto. Pojedyncza etykieta może służyć do obu operacji:

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

Ta funkcja została szeroko żądana w społeczności języka C#, a dyskusje sięgają dziesięcioleci, a temat jest wprowadzany i stale śledzony. Podobne funkcje istnieją w kilku innych nowoczesnych językach:

We wszystkich tych przypadkach języki działają tak samo jak w tej specyfikacji. Niektóre konstrukcje mogą mieć etykietę i można odwoływać się do tej etykiety z odpowiednich continue instrukcji lub break .

Szczegółowy projekt

Poniższe aktualizacje są prezentowane jako różnice w odpowiednich sekcjach standardu języka C# 7 (statements.md). W tej sekcji przekreślenie wskazuje, że tekst jest usuwany z istniejącej specyfikacji, a pogrubienie oznacza dodanie tekstu. Niezmieniona proza jest cytowana dosłownie dla kontekstu.

Instrukcje oznaczone etykietą §13.5

Wstaw następujący akapit bezpośrednio po istniejącym akapicie "Etykieta może zostać odwołana z goto instrukcji (§13.10.4) w zakresie etykiety.":

Jeśli instrukcja natychmiast zagnieżdżona w labeled_statement jest switch_statement (§13.8.3) lub iteration_statement (§13.9), instrukcja zagnieżdżona jest oznaczona identyfikatoremlabeled_statement. Break_statement (§13.10.2) lub continue_statement (§13.10.3) może określić taki identyfikator, aby odwoływać się do zawierającej instrukcji oznaczonej etykietą.

Uwaga: tylko instrukcja , która jest natychmiast zagnieżdżona w labeled_statement , jest oznaczona tym identyfikatorem. Na przykład, na przykład, tylko a: b: while (…) …b etykiety iteration_statement; a etykiety labeled_statementb: while (…) … wewnętrzny, który nie jest samym switch_statement lub iteration_statement. W związku z break a; tym lub continue a; pojawianie się w treści pętli nie jest celem instrukcji while . notatka końcowa

§13.10.2 Instrukcja przerwania

break_statement
    : 'break' identifier? ';'
    ;

Instrukcja break zamyka najbliższą otaczającą switchinstrukcję , while, do, for, lub foreach .

Instrukcja break kończy najbliższe otaczające switch_statement (§13.8.3) lub iteration_statement (§13.9) lub, jeśli określono identyfikator , najbliższe otaczające switch_statement lub iteration_statement oznaczone tym identyfikatorem (patrz §13.5).

Elementem docelowym instrukcji break jest punkt końcowy najbliższej instrukcji otaczającej switch, while, do, forlub foreach instrukcjiokreślonej jako powyżej. break Jeśli instrukcja nie jest ujęta w instrukcję switch, while, do, forlubforeach, wystąpi błąd czasu kompilacji.Jeśli taka instrukcja ujęta nie istnieje, wystąpi błąd czasu kompilacji.

Gdy wiele instrukcji switch, while, do, for lub foreach jest zagnieżdżonych jedna w drugą, instrukcja break ma zastosowanie tylko do najbardziej wewnętrznej instrukcji. Aby przenieść kontrolę na wiele poziomów zagnieżdżania, użyj instrukcji goto (§13.10.4).

Instrukcja break nie może zamknąć finally bloku (§13.11). break Gdy instrukcja występuje w ramach finally bloku, element docelowy instrukcji break powinien znajdować się w tym samym finally bloku; w przeciwnym razie wystąpi błąd kompilacji.

Instrukcja break jest wykonywana w następujący sposób:

  • Jeśli instrukcja break opuszcza jeden lub więcej bloków try ze skojarzonymi blokami finally, kontrola jest początkowo przekazywana do bloku finally najbardziej wewnętrznej instrukcji try. Gdy przepływ sterowania osiągnie punkt końcowy bloku finally, zostanie przeniesiony do bloku finally następnej otaczającej instrukcji try. Ten proces jest powtarzany, aż bloki wszystkich interweniujących finally instrukcji zostaną wykonane.
  • Kontrolka jest przenoszona do elementu docelowego instrukcji break .

Ponieważ instrukcja break bezwarunkowo przenosi kontrolę gdzie indziej, punkt końcowy instrukcji break nigdy nie jest osiągalny.

Przykład: etykieta jest rozpoznawana break jako najbliższa otaczana switch_statement lub iteration_statement z pasującą etykietą:

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

przykład końcowy

§13.10.3 Instrukcja continue

continue_statement
    : 'continue' identifier? ';'
    ;

Instrukcja continue rozpoczyna nową iterację najbliższej otaczającej whileinstrukcji , do, forlub foreach .

Instrukcja continue rozpoczyna nową iterację najbliższej otaczającej iteration_statement (§13.9) lub, jeśli określono identyfikator , najbliższą otaczającą iteration_statement oznaczona tym identyfikatorem (patrz §13.5).

Elementem docelowym instrukcji continue jest punkt końcowy osadzonej instrukcji najbliższej otaczającej whileinstrukcji , do, forlub foreachiteration_statement określonej jako powyżej. continue Jeśli instrukcja nie jest ujęta w instrukcję while, do, forlubforeach, wystąpi błąd czasu kompilacji.Jeśli taka instrukcja ujęta nie istnieje, wystąpi błąd czasu kompilacji.

Gdy wiele instrukcji while, do, for lub foreach jest zagnieżdżonych w sobie, instrukcja continue odnosi się tylko do najbardziej wewnętrznej instrukcji. Aby przenieść kontrolę na wiele poziomów zagnieżdżania, użyj instrukcji goto (§13.10.4).

Instrukcja continue nie może zamknąć finally bloku (§13.11). continue Gdy instrukcja występuje w ramach finally bloku, element docelowy instrukcji continue powinien znajdować się w tym samym finally bloku; w przeciwnym razie wystąpi błąd kompilacji.

Instrukcja continue jest wykonywana w następujący sposób:

  • Jeśli instrukcja continue opuszcza jeden lub więcej bloków try ze skojarzonymi blokami finally, kontrola jest początkowo przekazywana do bloku finally najbardziej wewnętrznej instrukcji try. Gdy przepływ sterowania osiągnie punkt końcowy bloku finally, zostanie przeniesiony do bloku finally następnej otaczającej instrukcji try. Ten proces jest powtarzany, aż bloki wszystkich interweniujących finally instrukcji zostaną wykonane.
  • Kontrolka jest przenoszona do elementu docelowego instrukcji continue .

Ponieważ instrukcja continue bezwarunkowo przenosi kontrolę gdzie indziej, punkt końcowy instrukcji continue nigdy nie jest osiągalny.

Przykład: etykieta jest rozpoznawana continue jako najbliższa otaczana iteration_statement z pasującą etykietą:

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

przykład końcowy

Wady/alternatywy

Zachowaj użycie goto instrukcji

Język C# obsługuje już program goto, który może wykonać ten sam przepływ sterowania. Jednak goto ma kilka wad w porównaniu z etykietami break/continue:

  • Wymaga oddzielnych etykiet dla scenariuszy przerwania i kontynuowania (przerywanie etykiet przechodzi po pętli, etykiety kontynuuj przed zamykającym nawiasem klamrowym)
  • Umieszczanie etykiet jest mniej intuicyjne i różni się w zależności od tego, czy się przerywasz, czy kontynuujesz
  • Mniej jawne informacje o intencji (przechodzenie do lokalizacji a przerywanie/kontynuowanie określonej pętli)
  • Brittle i podatne na błędy: deweloperzy nie muszą zagwarantować, że żadne instrukcje nie zostaną przypadkowo umieszczone między etykietami a ich konstrukcjami docelowymi. Na przykład, po goto END_LOOP; czym END_LOOP:następuje , łatwo jest przypadkowo wstawić między nimi instrukcję podczas konserwacji, przerywając zamierzony przepływ sterowania. Pętle oznaczone etykietami uniemożliwiają ten problem przez powiązanie etykiety bezpośrednio z konstrukcją.
  • Niesie historyczne piętno, którego etykieta break/continue unika

Używanie zmiennych flagi

Jak pokazano w sekcji motywacji, zmienne flag działają, ale dodają znaczną elementy kotłowe i zaciemniają logikę przepływu sterowania.

Używanie break N lub continue N z poziomami liczbowymi

  • Kruche podczas refaktoryzacji (dodawanie/usuwanie poziomu pętli wymaga zaktualizowania wszystkich odwołań liczbowych)
  • Trudniejsze do odczytania (musi zliczać poziomy, aby zrozumieć cel)
  • Mniej jawne niż nazwane etykiety
  • Brak jasności (oparty na 1? oparty na 0?)

Refaktoryzacja na oddzielne metody

Chociaż często jest to dobra praktyka, nie zawsze jest to możliwe lub odpowiednie, a czasami wprowadza niepotrzebną złożoność tego, co powinno być prostym przepływem sterowania.

Ta propozycja konsoliduje i zajmuje się następującymi dyskusjami społeczności:

Otwórz pytania

semantyka etykiet

Bieżąca specyfikacja definiuje semantykę jako break identifier/continue identifier znalezienie najbardziej wewnętrznej konstrukcji pętli/przełącznika oznaczonego tym identyfikatorem, a następnie wysłanie do niej standardowej break/continue semantyki. Alternatywna formalizacja polega na tym, aby zamiast tego powiedzieć, że break identifier/continue identifier identyfikuje etykietę przy użyciu tych samych reguł co "goto". Jeśli etykieta zawiera bezpośrednio konstrukcję pętli/przełącznika, która otacza przerwę/kontynuację, oznacza to pętlę/przełącznik, do którego ma zastosowanie przerwanie/kontynuowanie.

Zarówno formalizacja są rzeczywiście identyczne, pozwalając i nie zezwalając na ten sam zestaw programów. Podejście wybrane w tej specyfikacji zostało wykonane zarówno dla prostoty koncepcyjnej, jak i literału. Nie musi obejmować określania zakresu etykiet ani powiązania identyfikatorów, tak jak goto to robi, lub konieczności przeciwstawienia się logiki powiązania na zewnątrz w celu rozpoznawania pętli/przełącznika i instrukcji continue/break. Zamiast tego po prostu rozszerza prosty język specyfikacji, który znajduje odpowiednią otaczającą pętlę/przełącznik, biorąc pod uwagę przerwę/kontynuację, co pozwala na rozszerzenie przeszłości najbardziej wewnętrznej, na coś powyżej.

Jeśli LDM czuje się wiązanie tego mocniej do goto + etykiety semantyki, nie byłoby trudno dostosować specyfikację do tego. Utrzymanie tego pytania otwarte, jeśli grupa czuje, że ta ostatnia forma jest bardziej naturalna niż forma podjęta tutaj.

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

Specyfikacja sposobu deklarowanego etykiet:

Każdy blok lub switch_block tworzy oddzielną przestrzeń deklaracji dla etykiet. Nazwy są wprowadzane do tej przestrzeni deklaracji przez labeled_statements, a nazwy są przywołyne za pośrednictwem goto_statements. Przestrzeń deklaracji etykiety bloku zawiera wszystkie zagnieżdżone bloki. W związku z tym w zagnieżdżonym bloku nie można zadeklarować etykiety o takiej samej nazwie jak etykieta w otaczającym bloku.

Specyfikacje dotyczące instrukcji oznaczonych etykietami:

Zakres etykiety obejmuje cały blok, w którym etykieta jest zadeklarowana, łącznie z zagnieżdżonymi blokami. Jest to błąd czasu kompilacji, gdy dwie etykiety o tej samej nazwie mają nakładające się zakresy.
Etykietę można przywoływać z instrukcji goto (§13.10.4) w zakresie etykiety.

zagnieżdżone etykiety

Czy powinny a: b: while (true) continue a; być obsługiwane?

Zalecenie: nie. Żaden użytkownik nie poprosił o to. Nie przedstawiono żadnych atrakcyjnych przypadków użycia. Większość języków głównego nurtu nie pozwala na to, bez skarg ze strony swoich społeczności. Język lang (i impl) są prostsze i jaśniejsze, że tylko bezpośrednie etykiety instrukcji z etykietami pętla/przełącznik.

Spotkania dotyczące projektowania

TBD