Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 .
- Problem z mistrzem: https://github.com/dotnet/csharplang/issues/9875
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:
- Java: Rozgałęzianie instrukcji (Samouczek Oracle)
- JavaScript: instrukcja oznaczona etykietą (MDN)
- Kotlin: Zwraca i skoki
- Swift: Przepływ sterowania — instrukcje oznaczone etykietami
- Rust: Etykiety pętli
- Go: Instrukcje z etykietami
- Zig: Pętle oznaczone etykietami
- Dart: pętle
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 określonej jako powyżej.
switch, while, do, forlub foreach instrukcji
Jeśli taka instrukcja ujęta nie istnieje, wystąpi błąd czasu kompilacji.break Jeśli instrukcja nie jest ujęta w instrukcję switch, while, do, forlubforeach, 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
breakopuszcza jeden lub więcej blokówtryze skojarzonymi blokamifinally, kontrola jest początkowo przekazywana do blokufinallynajbardziej wewnętrznej instrukcjitry. Gdy przepływ sterowania osiągnie punkt końcowy blokufinally, zostanie przeniesiony do blokufinallynastępnej otaczającej instrukcjitry. Ten proces jest powtarzany, aż bloki wszystkich interweniującychfinallyinstrukcji 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
breakjako 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 iteration_statement określonej jako powyżej.
whileinstrukcji , do, forlub foreach
Jeśli taka instrukcja ujęta nie istnieje, wystąpi błąd czasu kompilacji.continue Jeśli instrukcja nie jest ujęta w instrukcję while, do, forlubforeach, 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
continueopuszcza jeden lub więcej blokówtryze skojarzonymi blokamifinally, kontrola jest początkowo przekazywana do blokufinallynajbardziej wewnętrznej instrukcjitry. Gdy przepływ sterowania osiągnie punkt końcowy blokufinally, zostanie przeniesiony do blokufinallynastępnej otaczającej instrukcjitry. Ten proces jest powtarzany, aż bloki wszystkich interweniującychfinallyinstrukcji 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
continuejako 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;czymEND_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.
Powiązane dyskusje i problemy
Ta propozycja konsoliduje i zajmuje się następującymi dyskusjami społeczności:
- Dyskusja nr 6634: Pętla zagnieżdżona w języku C#
- Problem nr 869: Dyskusja: Pętla zagnieżdżona w języku C#
- Dyskusja nr 5525: [Propozycja] Pętle z etykietami, takie jak w Java
- Problem nr 1597: [Propozycja] Pętle z etykietami, takie jak w Java
- Dyskusja nr 5521: przerwy w zagnieżdżonych pętlach z przerwą X, kontynuuj X
- Problem nr 4109: [Propozycja]: Cukier składniowy do zerwania lub kontynuowania zagnieżdżonych pętli
- Problem nr 3511: [Propozycja] "doublecontine" do contine zewnętrznej pętli
- Problem nr 2024: przerywanie i kontynuowanie inhancements
- Dyskusja nr 8434: Łańcuchowe instrukcje przepływu sterowania: break [, break]... [,kontynuuj]
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
C# feature specifications