Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Beschriftete
Note
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den relevanten Sprachentwurfsbesprechungen (LDM)-Notizen erfasst.
Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
- Problem mit Experten: https://github.com/dotnet/csharplang/issues/9875
Zusammenfassung
Zulassen break und continue Anweisungen können optional eine Bezeichnung angeben, die angibt, auf welche Schleife oder switch Anweisung ausgerichtet werden soll, wodurch ein saubererer Steuerungsfluss in geschachtelten Konstrukten ohne Anweisungen goto oder andere Verläufe wie geschachtelte Funktionen, Tupelrückläufe usw. ermöglicht wird.
Motivation
Beim Arbeiten mit geschachtelten Schleifen oder Schleifen, die Anweisungen enthalten switch , müssen Entwickler häufig eine äußere Schleife innerhalb eines inneren Kontexts ausbrechen oder fortsetzen. Derzeit gibt es zwei primäre Ansätze, um dies zu erreichen, beide mit erheblichen Nachteilen:
Verwenden von goto Anweisungen
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);
Während goto der Arbeit ist es erforderlich, Beschriftungen nach dem Schleifenkonstrukt zu platzieren und die Absicht, eine bestimmte Schleife zu unterbrechen, nicht eindeutig zu kommunizieren. Für die Fortsetzung einer äußeren Schleife wird der Ansatz noch ungünstiger:
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
goto CONTINUE_OUTER;
}
CONTINUE_OUTER: ;
}
Dieses Muster ist verwirrend, da die Beschriftung am Ende des Schleifentexts platziert werden muss, unmittelbar vor der schließenden Klammer, damit die Incrementor- und Bedingungsprüfung erfolgt. Wenn beide und breakcontinue für dieselbe äußere Schleife erforderlich sind, sind zwei separate Beschriftungen erforderlich:
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
Verwenden von Flagvariablen
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);
Dieser Ansatz erfordert zusätzliche Zustandsverwaltung, erhöht die Ausführlichkeit des Codes und verdeckt die Steuerungsflussabsicht.
Vorgeschlagene Lösung
Mit bezeichnungen break und continuewird der Code klarer und besser verwaltet:
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);
Die Bezeichnung wird direkt auf der Schleife platziert, die identifiziert wird, und die Break/Continue-Anweisung benennt das Ziel explizit. Weiter:
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
continue outer;
}
}
Dies drückt natürlich "die äußere Schleife fortsetzen" aus, ohne dass die Verwechslung der Bezeichnungsplatzierung zugeordnet ist goto. Eine einzelne Bezeichnung kann für beide Vorgänge verwendet werden:
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;
}
}
Dieses Feature wurde umfassend in der C#-Community angefordert, mit Diskussionen aus Jahrzehnten und dem Thema, das kontinuierlich eingeführt und erneut angefordert wird. Ähnliche Features sind in mehreren anderen modernen Sprachen vorhanden:
- Java: Verzweigungsanweisungen (Oracle-Lernprogramm)
- JavaScript: Labeled-Anweisung (MDN)
- Kotlin: Gibt zurück und springt
- Swift: Control Flow - Labeled Statements
- Rost: Schleifenbeschriftungen
- Gehe: Bezeichnungsanweisungen
- Zig: Gekennzeichnete Schleifen
- Dart: Schleifen
In all diesen Fällen funktionieren die Sprachen auf die gleiche Weise wie in dieser Spezifikation. Einige Konstrukte können nämlich über eine Bezeichnung verfügen, und es ist möglich, aus ihren jeweiligen continue Anweisungen break auf diese Bezeichnung zu verweisen.
Detailliertes Design
Die folgenden Updates werden als Diff für die entsprechenden Abschnitte des C# 7-Standards (statements.md) dargestellt.
In diesem Abschnitt gibt durchgestrichenen Text an, der aus der vorhandenen Spezifikation entfernt wird, und fett formatierten Text, der hinzugefügt wird. Unverändertes Prose wird für kontextbezogenes Verbatim zitiert.
§13.5 Bezeichnungsanweisungen
Fügen Sie den folgenden Absatz unmittelbar nach dem vorhandenen Absatz "Auf eine Bezeichnung kann aus goto Anweisungen (§13.10.4) innerhalb des Bereichs der Bezeichnung verwiesen werden":
Wenn die In einem labeled_statement sofort verschachtelte Anweisung ein switch_statement (§13.8.3) oder ein iteration_statement (§13.9) ist, wird die verschachtelte Anweisung mit dem Bezeichner des labeled_statementbezeichnet. Ein break_statement (§13.10.2) oder continue_statement (§13.10.3) kann einen solchen Bezeichner angeben, um auf die enthaltende beschriftete Anweisung zu verweisen.
Hinweis: Nur die Anweisung , die sofort in einem labeled_statement geschachtelt ist, wird mit diesem Bezeichner bezeichnet. Beispielsweise a: b: while (…) …beschrifte ich nur b die iteration_statement; a beschrifte die innere labeled_statementb: while (…) …, bei der es sich nicht um eine switch_statement oder iteration_statement handelt.
break a; Folglich wird die Anweisung nicht auf die while Anweisung ausgerichtet oder continue a; innerhalb des Schleifentexts angezeigt.
Hinweisende
§13.10.2 Die Unterbrechungserklärung
break_statement
: 'break' identifier? ';'
;
Die break Anweisung beendet die nächste eingeschlossene switch, , while, do, , foroder foreach Anweisung.
Die break Anweisung beendet den nächstgelegenen umschließenden switch_statement (§13.8.3) oder iteration_statement (§13.9) oder, wenn ein Bezeichner angegeben wird, das nächste eingeschlossene switch_statement oder iteration_statement mit diesem Bezeichner bezeichnet wird (siehe §13.5).
Das Ziel einer break Anweisung ist der Endpunkt der nächstgelegenen eingeschlossenen , die wie oben festgelegt wurde.
switch, , while, do, for, oder foreach AnweisungsanweisungWenn eine Wenn keine solche eingeschlossene Anweisung vorhanden ist, tritt ein Kompilierungszeitfehler auf.break Anweisung nicht von einem switch, while, , do, , foroder foreach einer Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.
Wenn mehrere switch, , while, do, foroder foreach Anweisungen miteinander verschachtelt sind, gilt eine break Anweisung nur für die innerste Anweisung. Um die Steuerung über mehrere verschachtelte Ebenen hinweg zu übertragen, wird eine goto-Anweisung (§13.10.4) verwendet.
Eine break Anweisung kann einen finally Block nicht beenden (§13.11). Wenn eine break Anweisung innerhalb eines finally Blocks auftritt, liegt das Ziel der break Anweisung innerhalb desselben finally Blocks; andernfalls tritt ein Kompilierungsfehler auf.
Eine break Anweisung wird wie folgt ausgeführt:
- Wenn die
break-Anweisung einen oder mehreretry-Blöcke mit zugeordnetenfinally-Blöcken verlässt, wird die Kontrolle zunächst an denfinally-Block der innerstentry-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt einesfinally-Blocks erreicht, wird die Kontrolle an denfinally-Block der nächsten einschließendentry-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis diefinallyBlöcke aller dazwischen liegendentryAnweisungen ausgeführt wurden. - Die Steuerung wird an das Ziel der
breakAnweisung übertragen.
Da eine break Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer break Aussage nie erreichbar.
Beispiel: Eine beschriftete
breakAuflösung wird in die nächste eingeschlossene switch_statement oder iteration_statement mit der entsprechenden Bezeichnung aufgelöst: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 } }Endbeispiel
§13.10.3 Die Fortsetzungserklärung
continue_statement
: 'continue' identifier? ';'
;
Die continue Anweisung beginnt eine neue Iteration der nächstgelegenen eingeschlossenen while, , do, , foroder foreach Anweisung.
Die continue Anweisung beginnt eine neue Iteration der nächstgelegenen umschließenden iteration_statement (§13.9) oder, wenn ein Bezeichner angegeben ist, die nächste umschließende iteration_statement mit diesem Bezeichner bezeichnet (siehe §13.5).
Das Ziel einer continue Anweisung ist der Endpunkt der eingebetteten Anweisung der nächstgelegenen eingeschlossenen iteration_statement wie oben bestimmt.
while, , do, foroder foreach AnweisungWenn eine Wenn keine solche eingeschlossene Anweisung vorhanden ist, tritt ein Kompilierungszeitfehler auf.continue Anweisung nicht durch ein while, do, , foroder foreach eine Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.
Wenn mehrere while, , do, foroder foreach Anweisungen miteinander verschachtelt sind, gilt eine continue Anweisung nur für die innerste Anweisung. Um die Steuerung über mehrere verschachtelte Ebenen hinweg zu übertragen, wird eine goto-Anweisung (§13.10.4) verwendet.
Eine continue Anweisung kann einen finally Block nicht beenden (§13.11). Wenn eine continue Anweisung innerhalb eines finally Blocks auftritt, liegt das Ziel der continue Anweisung innerhalb desselben finally Blocks; andernfalls tritt ein Kompilierungsfehler auf.
Eine continue Anweisung wird wie folgt ausgeführt:
- Wenn die
continue-Anweisung einen oder mehreretry-Blöcke mit zugeordnetenfinally-Blöcken verlässt, wird die Kontrolle zunächst an denfinally-Block der innerstentry-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt einesfinally-Blocks erreicht, wird die Kontrolle an denfinally-Block der nächsten einschließendentry-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis diefinallyBlöcke aller dazwischen liegendentryAnweisungen ausgeführt wurden. - Die Steuerung wird an das Ziel der
continueAnweisung übertragen.
Da eine continue Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer continue Aussage nie erreichbar.
Beispiel: Eine mit Bezeichnung versehene
continueAuflösung wird in die nächste eingeschlossene iteration_statement mit der entsprechenden Bezeichnung aufgelöst: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 } }Endbeispiel
Nachteile/Alternativen
Verwenden von goto Anweisungen beibehalten
C# unterstützt gotobereits , was denselben Kontrollfluss erreichen kann.
goto Hat jedoch mehrere Nachteile im Vergleich zu beschrifteten Unterbrechungen/Fortsetzungen:
- Erfordert separate Bezeichnungen für Unterbrechungs- und Fortsetzungsszenarien (Unterbrechungsbezeichnungen werden nach der Schleife fortgesetzt, Beschriftungen werden vor der schließenden geschweiften Klammer fortgesetzt)
- Die Beschriftungsplatzierung ist weniger intuitiv und unterscheidet sich je nachdem, ob Sie brechen oder fortfahren
- Weniger explizit über Absichten (Springen zu einer Position im Vergleich zum Unterbrechen/Fortsetzen einer bestimmten Schleife)
- Spröde und fehleranfällig: Entwickler müssen sicherstellen, dass keine Anweisungen versehentlich zwischen Bezeichnungen und ihren Zielkonstrukten platziert werden. So ist es beispielsweise einfach,
goto END_LOOP;END_LOOP:versehentlich eine Anweisung zwischen ihnen während der Wartung einzufügen und den beabsichtigten Kontrollfluss zu unterbrechen. Beschriftete Schleifen verhindern dieses Problem, indem die Bezeichnung direkt an das Konstrukt gebunden wird. - Trägt historisches Stigma, das gekennzeichnete Unterbrechungen/Fortsetzungsbeschriftungen verhindert
Verwenden von Flagvariablen
Wie im Abschnitt "Motivation" gezeigt, funktionieren Kennzeichnungsvariablen, fügen aber signifikante Textbausteine hinzu und verdecken die Steuerungsflusslogik.
Verwenden break N oder continue N mit numerischen Ebenen
- Während der Umgestaltung zerbrechlich (das Hinzufügen/Entfernen einer Schleifenebene erfordert das Aktualisieren aller numerischen Bezüge)
- Schwieriger zu lesen (muss Ebenen zählen, um das Ziel zu verstehen)
- Weniger explizit als benannte Bezeichnungen
- Mangelnde Klarheit (1-basiert? 0-basiert?)
In separate Methoden umgestalten
Dies ist zwar häufig eine bewährte Methode, ist aber nicht immer machbar oder angemessen und führt manchmal zu unnötiger Komplexität für den einfachen Steuerungsfluss.
Verwandte Diskussionen und Probleme
Dieser Vorschlag konsolidiert und befasst sich mit den folgenden Communitydiskussionen:
- Diskussion #6634: Geschachtelte C#-Unterbrechungsschleife
- Problem #869: Diskussion: Geschachtelte C#-Unterbrechungsschleife
- Diskussion Nr. 5525: [Vorschlag] Beschriftete Schleifen wie in Java
- Problem Nr. 1597: [Vorschlag] Beschriftete Schleifen wie in Java
- Diskussion #5521: Geschachtelte Schleifen unterbrechungen mit Unterbrechung X, fortfahren X
- Problem Nr. 4109: [Vorschlag]: Syntaktischer Zucker zum Ausbrechen oder Fortsetzen geschachtelter Schleifen
- Problem Nr. 3511: [Vorschlag] "doublecontine", to contine outer loop
- Problem Nr. 2024: Unterbrechen und Fortsetzen von Inhancen
- Diskussion #8434: Verkettete Ablaufanweisungen: Break [, Break]... [,fortfahren]
Offene Fragen
Bezeichnungssemantik
Die aktuelle Spezifikation definiert die Semantik so break identifier/continue identifier , dass die innerste anwendbare Schleife/Switch-Konstrukt gefunden wird, die mit diesem Bezeichner beschriftet ist, und dann mit standardsemantischer break/continue Semantik an sie verteilt wird. Eine alternative Formalisierung besteht darin, stattdessen zu sagen, dass break identifier/continue identifier eine Bezeichnung mit denselben Regeln wie "goto" identifiziert wird. Und wenn die Beschriftung direkt ein Schleifen-/Schalterkonstrukt enthält, das den Umbruch/Den Vorgang einschließt, dann ist das die Schleife/der Schalter, auf den der Umbruch/Fortsetzung angewendet wird.
Beide Formalisierungen sind effektiv identisch, sodass die gleiche Reihe von Programmen zugelassen und deaktiviert wird. Der in dieser Spezifikation gewählte Ansatz wurde sowohl für konzeptionelle als auch für die literale Einfachheit getan. Es muss weder die Bereichsdefinition von Bezeichnungen noch die Bindung von Bezeichnern abdecken, oder goto die Bindung von Bezeichnern müssen oder die Bindung nach außen nach außen gerichteter Bindungslogik zum Auflösen der Schleife/des Schalters und der Continue/Break-Anweisung definieren. Stattdessen wird einfach die einfache Spezifikationssprache erweitert, die die entsprechende umschließende Schleife bzw. den entsprechenden Umschalter findet, wenn der Umbruch/Fortsetzung gegeben ist, sodass sie sich über das Innere hinaus erstrecken kann.
Wenn LDM diese engere Verbindung mit der Semantik von Goto+Label hat, wäre es nicht schwierig, die Spezifikation an dies anzupassen. Diese Frage offen zu halten, wenn die Gruppe die letztere Form hat, ist natürlicher als die hier aufgenommene Form.
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?
}
}
Geben Sie an , wie Bezeichnungen deklariert werden:
Jeder Block oder switch_block erstellt einen separaten Deklarationsraum für Bezeichnungen. Namen werden über labeled_statements in diesen Deklarationsbereich eingeführt, und auf die Namen wird über goto_statements verwiesen. Der Beschriftungsdeklarationsbereich eines Blocks enthält alle geschachtelten Blöcke. Daher ist es in einem geschachtelten Block nicht möglich, eine Bezeichnung mit demselben Namen wie eine Beschriftung in einem eingeschlossenen Block zu deklarieren.
Angeben von beschrifteten Anweisungen:
Der Bereich einer Bezeichnung ist der gesamte Block, in dem die Bezeichnung deklariert wird, einschließlich aller geschachtelten Blöcke. Es ist ein Kompilierungsfehler, wenn zwei Labels mit demselben Namen überlappende Gültigkeitsbereiche haben.
Auf eine Bezeichnung kann im Rahmen der Bezeichnung von Goto-Anweisungen (§13.10.4) verwiesen werden.
Geschachtelte Bezeichnungen
Sollte a: b: while (true) continue a; unterstützt werden?
Empfehlung: Nein. Dazu haben keine Benutzer gefragt. Dafür wurden keine überzeugenden Anwendungsfälle vorgestellt. Die meisten Mainstream-Sprachen lassen sie nicht zu, ohne Beschwerden von ihren Gemeinschaften. Die lang (und impl) sind einfacher und klarer, wenn nur die direkt enthaltende beschriftete Anweisung die Schleife/den Schalter beschriftet.
Planungsbesprechungen
TBD
C# feature specifications