Beschriftete break und continue Anweisungen

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.

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:

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 switch, , while, do, for, oder foreach Anweisungsanweisung, die wie oben festgelegt wurde. Wenn eine break Anweisung nicht von einem switch, while, , do, , foroder foreach einer Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.Wenn keine solche eingeschlossene Anweisung vorhanden 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 mehrere try-Blöcke mit zugeordneten finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
  • Die Steuerung wird an das Ziel der break Anweisung übertragen.

Da eine break Aussage die Kontrolle an anderer Stelle bedingungslos überträgt, ist der Endpunkt einer break Aussage nie erreichbar.

Beispiel: Eine beschriftete break Auflö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 while, , do, foroder foreach Anweisungiteration_statement wie oben bestimmt. Wenn eine continue Anweisung nicht durch ein while, do, , foroder foreach eine Anweisung eingeschlossen ist, tritt ein Kompilierungszeitfehler auf.Wenn keine solche eingeschlossene Anweisung vorhanden 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 mehrere try-Blöcke mit zugeordneten finally-Blöcken verlässt, wird die Kontrolle zunächst an den finally-Block der innersten try-Anweisung übertragen. Wenn und falls die Kontrolle den Endpunkt eines finally-Blocks erreicht, wird die Kontrolle an den finally-Block der nächsten einschließenden try-Anweisung übertragen. Dieser Vorgang wird wiederholt, bis die finally Blöcke aller dazwischen liegenden try Anweisungen ausgeführt wurden.
  • Die Steuerung wird an das Ziel der continue Anweisung ü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 continue Auflö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.

Dieser Vorschlag konsolidiert und befasst sich mit den folgenden Communitydiskussionen:

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