Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Note
Questo articolo è una specifica delle funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono riportate nelle note pertinenti della riunione di progettazione linguistica (LDM) .
Ulteriori dettagli sul processo di adozione delle specifiche di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .
- Problema con campioni: https://github.com/dotnet/csharplang/issues/9875
Sommario
Consenti break alle istruzioni e continue di specificare facoltativamente un'etichetta che identifica il ciclo o switch l'istruzione di destinazione, consentendo un flusso di controllo più pulito in costrutti annidati senza richiedere goto istruzioni o altre contorzioni come funzioni nidificate, ritorni di tupla e così via.
Motivazione
Quando si usano cicli o cicli annidati contenenti switch istruzioni, gli sviluppatori spesso devono interrompere o continuare un ciclo esterno dall'interno di un contesto interno. Attualmente, esistono due approcci principali per ottenere questo risultato, entrambi con svantaggi significativi:
Istruzioni Using 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);
Mentre goto funziona, richiede l'inserimento di etichette dopo il costrutto del ciclo e non comunica chiaramente la finalità di interrompere da un ciclo specifico. Per continuare un ciclo esterno, l'approccio diventa ancora più imbarazzante:
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
goto CONTINUE_OUTER;
}
CONTINUE_OUTER: ;
}
Questo modello crea confusione perché l'etichetta deve essere posizionata alla fine del corpo del ciclo, subito prima della parentesi graffa di chiusura, in modo che si verifichi il incrementore e il controllo delle condizioni. Quando sono necessari entrambi break e continue per lo stesso ciclo esterno, sono necessarie due etichette separate:
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
Uso delle variabili di flag
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);
Questo approccio richiede una gestione aggiuntiva dello stato, aumenta il livello di dettaglio del codice e nasconde la finalità del flusso di controllo.
Soluzione proposta
Con etichetta break e continue, il codice diventa più chiaro e gestibile:
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);
L'etichetta viene inserita direttamente nel ciclo identificato e l'istruzione break/continue assegna in modo esplicito un nome alla destinazione. Per continuare:
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
continue outer;
}
}
Questo esprime naturalmente "continua il ciclo esterno", senza la confusione del posizionamento delle etichette associato a goto. Una singola etichetta può essere usata per entrambe le operazioni:
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;
}
}
Questa funzionalità è stata richiesta ampiamente nella community C#, con discussioni che risalgono a decenni e l'argomento che viene reintrodotto e richiesto continuamente. Esistono funzionalità simili in diversi altri linguaggi moderni:
- Java: Istruzioni di diramazione (esercitazione su Oracle)
- JavaScript: istruzione con etichetta (MDN)
- Kotlin: restituisce e salti
- Swift: Flusso di controllo - Istruzioni etichettate
- Rust: Etichette ciclo
- Go: istruzioni etichettate
- Zig: cicli etichettati
- Dart: Cicli
In tutti questi casi, le lingue operano nello stesso modo di in questa specifica. Vale a dire, alcuni costrutti possono avere un'etichetta ed è possibile fare riferimento a tale etichetta dalle rispettive continue istruzioni o break .
Progettazione dettagliata
Gli aggiornamenti seguenti vengono presentati come differenze rispetto alle sezioni corrispondenti dello standard C# 7 (statements.md).
In questa sezione, barrato indica che il testo viene rimosso dalla specifica esistente e il grassetto indica che il testo viene aggiunto. La prosa non modificata è racchiusa tra virgolette per il contesto.
Istruzioni etichettate di §13.5
Inserire il paragrafo seguente immediatamente dopo il paragrafo esistente "A label can be referenced from goto statements (§13.10.4) within the scope of the label.":
Se l'istruzione annidata immediatamente all'interno di un labeled_statement è un switch_statement (§13.8.3) o un iteration_statement (§13.9), l'istruzione nidificata viene etichettata conl'identificatore del labeled_statement. Un break_statement (§13.10.2) o continue_statement (§13.10.3) può specificare tale identificatore per fare riferimento all'istruzione etichettata contenitore.
Nota: solo l'istruzione annidata immediatamente all'interno di un labeled_statement viene etichettata con tale identificatore. Ad esempio, dato a: b: while (…) …, etichetta solo b il iteration_statement; a etichetta il labeled_statementb: while (…) … interno, che non è un switch_statement o un iteration_statement. Di conseguenza, break a; o continue a; la visualizzazione all'interno del corpo del ciclo non ha come destinazione l'istruzione while .
nota finale
§13.10.2 Istruzione break
break_statement
: 'break' identifier? ';'
;
L'istruzione break esce dall'istruzione , , whiledofor, o foreach più switchvicina.
L'istruzione break esce dal switch_statement di inclusione più vicina (§13.8.3) o iteration_statement (§13.9) o, se viene specificato un identificatore , il switch_statement più vicino o iteration_statement etichettato con tale identificatore (vedere §13.5).
La destinazione di un'istruzione break è il punto finale dell'istruzione di inclusione determinata in precedenza.
switchpiù vicina, , whiledo, for, o foreachSe un'istruzione Se non esiste un'istruzione di inclusione di questo tipo, si verifica un errore in fase di compilazione.break non è racchiusa da un'istruzione switch, while, dofor, o foreach , si verifica un errore in fase di compilazione.
Quando istruzioni multiple come switch, while, do, for o foreach sono annidate l'una dentro l'altra, un'istruzione break si applica solo all'istruzione più interna. Per trasferire il controllo tra più livelli di annidamento, verrà utilizzata un'istruzione goto (§13.10.4).
Un'istruzione break non può uscire da un finally blocco (§13.11). Quando si verifica un'istruzione break all'interno di un finally blocco, la destinazione dell'istruzione break deve trovarsi all'interno dello stesso finally blocco; in caso contrario, si verifica un errore in fase di compilazione.
Un'istruzione break viene eseguita come segue:
- Se l'istruzione
break, esce da uno o più blocchitryche hanno blocchi associatifinally, il controllo viene inizialmente trasferito al bloccofinallydell'istruzionetrypiù interna. Quando e se il controllo raggiunge il punto finale di unfinallyblocco, il controllo viene trasferito alfinallyblocco dell'istruzione di inclusionetrysuccessiva. Questo processo viene ripetuto fino a quando non vengono eseguiti ifinallyblocchi di tutte le istruzioni intermedietry. - Il controllo viene trasferito alla destinazione dell'istruzione
break.
Poiché un'istruzione break trasferisce in modo incondizionato il controllo altrove, il punto finale di un'istruzione break non è mai raggiungibile.
Esempio: un oggetto etichettato
breakviene risolto nel switch_statement più vicino o iteration_statement con l'etichetta corrispondente: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 } }esempio finale
§13.10.3 Istruzione continue
continue_statement
: 'continue' identifier? ';'
;
L'istruzione continue avvia una nuova iterazione dell'istruzione contenitore più whilevicina, , fordo, o foreach .
L'istruzione continue avvia una nuova iterazione del iteration_statement di inclusione più vicina (§13.9) oppure, se viene specificato un identificatore , il più vicino iteration_statement racchiuso con tale identificatore (vedere §13.5).
La destinazione di un'istruzione continue è il punto finale dell'istruzione incorporata dell'istruzione contenitore più iteration_statement determinata come indicato in precedenza.
whilevicina , , dofor, o foreachSe un'istruzione Se non esiste un'istruzione di inclusione di questo tipo, si verifica un errore in fase di compilazione.continue non è racchiusa da un'istruzione while, do, foro foreach , si verifica un errore in fase di compilazione.
Quando più whileistruzioni , do, foro foreach sono annidate tra loro, un'istruzione continue si applica solo all'istruzione più interna. Per trasferire il controllo tra più livelli di annidamento, verrà utilizzata un'istruzione goto (§13.10.4).
Un'istruzione continue non può uscire da un finally blocco (§13.11). Quando si verifica un'istruzione continue all'interno di un finally blocco, la destinazione dell'istruzione continue deve trovarsi all'interno dello stesso finally blocco; in caso contrario, si verifica un errore in fase di compilazione.
Un'istruzione continue viene eseguita come segue:
- Se l'istruzione
continue, esce da uno o più blocchitryche hanno blocchi associatifinally, il controllo viene inizialmente trasferito al bloccofinallydell'istruzionetrypiù interna. Quando e se il controllo raggiunge il punto finale di unfinallyblocco, il controllo viene trasferito alfinallyblocco dell'istruzione di inclusionetrysuccessiva. Questo processo viene ripetuto fino a quando non vengono eseguiti ifinallyblocchi di tutte le istruzioni intermedietry. - Il controllo viene trasferito alla destinazione dell'istruzione
continue.
Poiché un'istruzione continue trasferisce in modo incondizionato il controllo altrove, il punto finale di un'istruzione continue non è mai raggiungibile.
Esempio: un oggetto etichettato
continueviene risolto nel iteration_statement di inclusione più vicino con l'etichetta corrispondente: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 } }esempio finale
Svantaggi/alternative
Mantenere le istruzioni using goto
C# supporta gotogià , che può eseguire lo stesso flusso di controllo. Tuttavia, goto presenta diversi svantaggi rispetto all'interruzione/continuazione etichettata:
- Richiede etichette separate per gli scenari di interruzione e continuazione (le etichette di interruzione vanno dopo il ciclo, continuare le etichette prima della parentesi graffa di chiusura)
- Il posizionamento delle etichette è meno intuitivo e differisce in base al fatto che si stia interrompendo o continuando
- Meno esplicito sulla finalità (passaggio a una posizione e interruzione/continuazione di un ciclo specifico)
- Brittle e soggetto a errori: gli sviluppatori devono assicurarsi che non vengano accidentalmente posizionate istruzioni tra le etichette e i costrutti di destinazione. Ad esempio, con
goto END_LOOP;seguito daEND_LOOP:, è facile inserire inavvertitamente un'istruzione tra di esse durante la manutenzione, interrompendo il flusso di controllo previsto. I cicli etichettati impediscono questo problema associando l'etichetta direttamente al costrutto. - Porta lo stigma cronologico con etichetta di interruzione/continuazione evita
Usare le variabili di flag
Come illustrato nella sezione motivazione, le variabili flag funzionano, ma aggiungono un boilerplate significativo e oscurano la logica del flusso di controllo.
Usare break N o continue N con livelli numerici
- Fragile durante il refactoring (l'aggiunta/rimozione di un livello di ciclo richiede l'aggiornamento di tutti i riferimenti numerici)
- Più difficile da leggere (deve contare i livelli per comprendere la destinazione)
- Meno esplicito rispetto alle etichette denominate
- Mancanza di chiarezza (basata su 1? basata su 0?)
Eseguire il refactoring in metodi separati
Sebbene questa sia spesso una procedura consigliata, non è sempre fattibile o appropriata e talvolta introduce una complessità non necessaria per ciò che deve essere semplice flusso di controllo.
Discussioni e problemi correlati
Questa proposta consolida e affronta le discussioni della comunità seguenti:
- Discussione #6634: Ciclo annidato di interruzione C#
- Problema n. 869: Discussione: Ciclo annidato di interruzione C#
- Discussione #5525: [Proposta] Cicli etichettati come in Java
- Problema 1597: [Proposta] Cicli etichettati come in Java
- Discussione n. 5521: interruzioni di cicli annidati con interruzione X, continua X
- Problema n. 4109: [Proposta]: Zucchero sintattico per la rottura o il continuo ciclo annidato
- Problema 3511: [Proposta] "doublecontine", per contine outer loop
- Problema 2024: interruzione e continuazione delle interruzioni
- Discussione #8434: Istruzioni del flusso di controllo concatenato: break [, break]... [,continua]
Domande aperte
semantica delle etichette
La specifica corrente definisce la semantica per come break identifier/continue identifier trovare il costrutto loop/switch più interno applicabile etichettato con tale identificatore e quindi inviarlo con semantica standard.break/continue Una formalizzazione alternativa consiste nel dire che break identifier/continue identifier identifica un'etichetta usando le stesse regole di 'goto'. E se l'etichetta contiene direttamente un costrutto loop/switch che racchiude l'interruzione/continuazione, questo è il ciclo/interruttore a cui si applica l'interruzione/continuazione.
Entrambe le formalizzazioni sono effettivamente identiche, consentendo e disallowing lo stesso set di programmi. L'approccio scelto in questa speclet è stato fatto sia per semplicità concettuale che letterale. Non è necessario coprire l'ambito delle etichette o l'associazione di identificatori così come goto lo fa, o dover definire la logica di associazione verso l'esterno per la risoluzione del ciclo/opzione e dell'istruzione continua/interruzione. Al contrario, espande semplicemente il linguaggio delle specifiche semplice che trova il ciclo di inclusione/interruttore appropriato dato l'interruzione/continuazione, consentendo di estenderlo oltre l'interno, a qualcosa di sopra di esso.
Se LDM si sente legare questa semantica più stretta alla semantica goto+label, non sarebbe difficile regolare la specifica in questo modo. Mantenere aperta questa domanda se il gruppo ritiene che quest'ultima forma sia più naturale della forma presa qui.
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?
}
}
Specificare come vengono dichiarate le etichette:
Ogni blocco o switch_block crea uno spazio di dichiarazione separato per le etichette. I nomi vengono introdotti in questo spazio di dichiarazione tramite labeled_statements e i nomi vengono indicati tramite goto_statements. Lo spazio di dichiarazione di etichetta di un blocco include tutti i blocchi annidati. Pertanto, all'interno di un blocco annidato non è possibile dichiarare un'etichetta con lo stesso nome di un'etichetta in un blocco di inclusione.
Specifica per le istruzioni etichettate:
L'ambito di un'etichetta è l'intero blocco in cui viene dichiarata l'etichetta, inclusi i blocchi annidati. È un errore in fase di compilazione se due etichette con lo stesso nome hanno ambiti sovrapposti.
È possibile fare riferimento a un'etichetta dalle istruzioni goto (§13.10.4) all'interno dell'ambito dell'etichetta.
etichette annidate
Deve a: b: while (true) continue a; essere supportato?
Raccomandazione: no. Nessun utente ha chiesto questo. Non sono stati presentati casi d'uso accattivanti. La maggior parte delle lingue tradizionali non lo consente, senza lamentele dalle loro comunità. Il linguaggio (e impl) sono più semplici e più chiare portano rigorosamente che solo l'istruzione diretta contenente etichette etichettate il ciclo/opzione.
Incontri di design
TBD
C# feature specifications