Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Note
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas LDM (reunião de design de idioma) pertinentes.
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações.
- Problema defendido: https://github.com/dotnet/csharplang/issues/9875
Resumo
Permita break e continue as instruções especifique opcionalmente um rótulo que identifique qual loop ou switch instrução deve ser direcionada, permitindo o fluxo de controle mais limpo em constructos aninhados sem a necessidade goto de instruções ou outras contorções como funções aninhadas, retornos de tupla etc.
Motivação
Ao trabalhar com loops aninhados ou loops que contêm switch instruções, os desenvolvedores geralmente precisam sair ou continuar um loop externo de dentro de um contexto interno. Atualmente, há duas abordagens primárias para conseguir isso, ambas com desvantagens significativas:
Usando goto instruções
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);
Enquanto goto funciona, ele requer a colocação de rótulos após o constructo do loop e não comunica claramente a intenção de interromper de um loop específico. Para continuar um loop externo, a abordagem torna-se ainda mais estranha:
for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
goto CONTINUE_OUTER;
}
CONTINUE_OUTER: ;
}
Esse padrão é confuso porque o rótulo deve ser colocado no final do corpo do loop, logo antes da chave de fechamento, para que o incrementor e a verificação de condição ocorram. Quando ambos break e continue são necessários para o mesmo loop externo, dois rótulos separados são necessários:
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
Usando variáveis de sinalizador
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);
Essa abordagem requer gerenciamento de estado adicional, aumenta a verbosidade de código e obscurece a intenção de fluxo de controle.
Solução proposta
Com rotulado break e continue, o código torna-se mais claro e mais mantenedível:
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);
O rótulo é colocado diretamente no loop identificado e a instrução break/continue nomeia explicitamente seu destino. Para continuar:
outer: for (int x = 0; x < xMax; x++)
{
for (int y = 0; y < yMax; y++)
{
if (ShouldSkipRest(x, y))
continue outer;
}
}
Isso expressa naturalmente "continuar o loop externo", sem a confusão de posicionamento do rótulo associado gotoa . Um único rótulo pode ser usado para ambas as operações:
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;
}
}
Esse recurso foi amplamente solicitado na comunidade C#, com discussões que datam de décadas atrás e o tópico sendo reintroduzido e re-solicitado continuamente. Existem recursos semelhantes em várias outras linguagens modernas:
- Java: Instruções de ramificação (Tutorial do Oracle)
- JavaScript: Instrução rotulada (MDN)
- Kotlin: Retorna e pula
- Swift: Fluxo de Controle – Instruções rotuladas
- Rust: Rótulos de loop
- Go: instruções rotuladas
- Zig: Loops rotulados
- Dardo: Loops
Em todos esses casos, os idiomas operam da mesma maneira que nesta especificação. Ou seja, alguns constructos podem ter um rótulo e é possível referenciar esse rótulo de suas respectivas continue instruções ou break instruções.
Design detalhado
As atualizações a seguir são apresentadas como uma diferença em relação às seções correspondentes do padrão C# 7 (statements.md).
Ao longo desta seção, o tachado indica o texto sendo removido da especificação existente e negrito indica o texto que está sendo adicionado. A prosa inalterada é citada verbatim para contexto.
§13.5 Instruções rotuladas
Insira o parágrafo a seguir imediatamente após o parágrafo existente "Um rótulo pode ser referenciado de goto instruções (§13.10.4) dentro do escopo do rótulo.":
Se a instrução imediatamente aninhada em um labeled_statement for um switch_statement (§13.8.3) ou um iteration_statement (§13.9), a instrução aninhada será rotulada com o identificador do labeled_statement. Um break_statement (§13.10.2) ou continue_statement (§13.10.3) pode especificar esse identificador para referenciar a instrução rotulada que contém.
Observação: somente a instrução aninhada imediatamente em um labeled_statement é rotulada com esse identificador. Por exemplo, dado a: b: while (…) …, apenas b rotula o iteration_statement; a rotula o labeled_statementb: while (…) … interno, que não é em si um switch_statement ou iteration_statement. Consequentemente, break a; ou continue a; aparecer dentro do corpo do loop não tem como destino a while instrução.
nota final
§13.10.2 A instrução break
break_statement
: 'break' identifier? ';'
;
A break instrução sai da instrução , ou instruçãofordoswitchwhile, entre foreach outros.
A break instrução sai do switch_statement delimitador mais próximo (§13.8.3) ou iteration_statement (§13.9) ou, se um identificador for especificado, o switch_statement ou iteration_statement delimitador mais próximo rotulado com esse identificador (consulte §13.5).
O destino de uma break instrução é o ponto final da instrução delimitantemais próxima determinada como acima.
switch, while, doou foreachforinstruçãoSe uma Se não existir tal instrução delimitação, ocorrerá um erro de tempo de compilação.break instrução não estiver entre um switch, , while, doou forforeach instrução, ocorrerá um erro de tempo de compilação.
Quando várias switchinstruções , while, do, for, ou foreach são aninhadas umas nas outras, uma break instrução se aplica somente à instrução mais interna. Para transferir o controle entre vários níveis de aninhamento, uma goto instrução (§13.10.4) deve ser usada.
Uma break instrução não pode sair de um finally bloco (§13.11). Quando uma break instrução ocorre dentro de um finally bloco, o break destino da instrução deve estar dentro do mesmo finally bloco; caso contrário, ocorre um erro em tempo de compilação.
Uma break instrução é executada da seguinte maneira:
- Se a
breakinstrução sair de um ou maistryblocos com blocos associadosfinally, o controle será inicialmente transferido para ofinallybloco da instrução maistryinterna. Quando e se o controle atingir o ponto final de umfinallybloco, o controle será transferido para ofinallybloco da próxima instrução delimitadoratry. Esse processo é repetido até que osfinallyblocos de todas as instruções intermediáriastrytenham sido executados. - O controle é transferido para o destino da
breakinstrução.
Como uma break instrução transfere incondicionalmente o controle para outro lugar, o ponto final de uma break instrução nunca é alcançável.
Exemplo: um rótulo é
breakresolvido para o switch_statement ou iteration_statement delimitado mais próximo com o rótulo correspondente: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 } }exemplo de fim
§13.10.3 A instrução continue
continue_statement
: 'continue' identifier? ';'
;
A continue instrução inicia uma nova iteração do delimitamentoforwhiledo, ou foreach instrução mais próximo.
A continue instrução inicia uma nova iteração do iteration_statement delimitador mais próximo (§13.9) ou, se um identificador for especificado, o iteration_statement delimitador mais próximo rotulado com esse identificador (consulte §13.5).
O destino de uma continue instrução é o ponto final da instrução inserida da instrução delimitada iteration_statement determinada como acima.
whiledoformais próxima, ou foreachSe uma Se não existir tal instrução delimitação, ocorrerá um erro de tempo de compilação.continue instrução não estiver entre um while, , doforou foreach uma instrução, ocorrerá um erro de tempo de compilação.
Quando várias whileinstruções , do, for, ou foreach são aninhadas umas nas outras, uma continue instrução se aplica somente à instrução mais interna. Para transferir o controle entre vários níveis de aninhamento, uma goto instrução (§13.10.4) deve ser usada.
Uma continue instrução não pode sair de um finally bloco (§13.11). Quando uma continue instrução ocorre dentro de um finally bloco, o continue destino da instrução deve estar dentro do mesmo finally bloco; caso contrário, ocorre um erro em tempo de compilação.
Uma continue instrução é executada da seguinte maneira:
- Se a
continueinstrução sair de um ou maistryblocos com blocos associadosfinally, o controle será inicialmente transferido para ofinallybloco da instrução maistryinterna. Quando e se o controle atingir o ponto final de umfinallybloco, o controle será transferido para ofinallybloco da próxima instrução delimitadoratry. Esse processo é repetido até que osfinallyblocos de todas as instruções intermediáriastrytenham sido executados. - O controle é transferido para o destino da
continueinstrução.
Como uma continue instrução transfere incondicionalmente o controle para outro lugar, o ponto final de uma continue instrução nunca é alcançável.
Exemplo: um rótulo é
continueresolvido para o iteration_statement delimitado mais próximo com o rótulo correspondente: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 } }exemplo de fim
Desvantagens/alternativas
Continuar usando goto instruções
O C# já dá gotosuporte, o que pode realizar o mesmo fluxo de controle. No entanto, goto tem várias desvantagens em comparação com a interrupção/continuação rotulada:
- Requer rótulos separados para cenários de interrupção vs. continuar (rótulos de interrupção vão após o loop, continuar rótulos ir antes da chave de fechamento)
- O posicionamento do rótulo é menos intuitivo e difere com base em se você está quebrando ou continuando
- Menos explícito sobre a intenção (saltando para um local versus quebrando/continuando um loop específico)
- Frágil e propenso a erros: os desenvolvedores devem garantir que nenhuma instrução seja colocada acidentalmente entre rótulos e seus constructos de destino. Por exemplo, com
goto END_LOOP;seguido porEND_LOOP:, é fácil inserir inadvertidamente uma instrução entre eles durante a manutenção, quebrando o fluxo de controle pretendido. Os loops rotulados impedem esse problema associando o rótulo diretamente ao constructo. - Carrega o estigma histórico que a quebra/continuação rotulada evita
Usar variáveis de sinalizador
Conforme mostrado na seção de motivação, as variáveis de sinalizador funcionam, mas adicionam clichês significativos e obscurecem a lógica de fluxo de controle.
Usar break N ou continue N com níveis numéricos
- Frágil durante a refatoração (adicionar/remover um nível de loop requer a atualização de todas as referências numéricas)
- Mais difícil de ler (deve contar níveis para entender o destino)
- Menos explícito do que rótulos nomeados
- Falta de clareza (baseado em 1? baseado em 0?)
Refatorar em métodos separados
Embora isso seja geralmente uma boa prática, nem sempre é viável ou apropriado e, às vezes, introduz complexidade desnecessária para o que deve ser um fluxo de controle simples.
Discussões e problemas relacionados
Esta proposta consolida e aborda as seguintes discussões da comunidade:
- Discussão nº 6634: Loop aninhado de quebra do C#
- Problema nº 869: Discussão: Loop aninhado de quebra do C#
- Discussão nº 5525: [Proposta] Loops rotulados como em Java
- Problema nº 1597: [Proposta] Loops rotulados como em Java
- Discussão nº 5521: interrupção de loops aninhados com quebra X, continuar X
- Problema nº 4109: [Proposta]: açúcar sintactico para quebrar ou continuar loops aninhados
- Problema nº 3511: [Proposta] "doublecontine", para continar loop externo
- Problema nº 2024: interromper e continuar inhancements
- Discussão nº 8434: Instruções de fluxo de controle encadeado: quebra [, quebra]... [,continue]
Perguntas abertas
semântica de rótulo
A especificação atual define a semântica para break identifier/continue identifier localizar o constructo de loop/comutador mais interno aplicável rotulado com esse identificador e, em seguida, expedindo para ele com semântica padrão break/continue . Uma formalização alternativa é, em vez disso, dizer que break identifier/continue identifier identifica um rótulo usando as mesmas regras que 'goto'. E se o rótulo contiver diretamente um constructo de loop/comutador que inclui a quebra/continuação, esse será o loop/comutador ao qual a quebra/continua se aplica.
Ambas as formalizações são efetivamente idênticas, permitindo e não permitindo o mesmo conjunto de programas. A abordagem escolhida neste espectro foi feita para simplicidade conceitual e literal. Ele não precisa abranger o escopo de rótulos ou a associação de identificadores como goto ele faz ou ter que definir a lógica de associação externa para resolver a instrução loop/switch e continue/break. Em vez disso, ele simplesmente expande a linguagem de especificação simples que localiza o loop/comutador delimitador apropriado dado o intervalo/continuação, permitindo que ele se estenda além do mais interno, para algo acima disso.
Se o LDM sentir a associação mais apertada à semântica goto+label, não seria difícil ajustar a especificação a isso. Manter essa pergunta aberta se o grupo achar que a última forma é mais natural do que a forma tomada aqui.
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?
}
}
Especificação sobre como os rótulos são declarados:
Cada bloco ou switch_block cria um espaço de declaração separado para rótulos. Os nomes são introduzidos nesse espaço de declaração por meio de labeled_statements e os nomes são referenciados por meio de goto_statements. O espaço de declaração de rótulo de um bloco inclui todos os blocos aninhados. Assim, dentro de um bloco aninhado, não é possível declarar um rótulo com o mesmo nome de um rótulo em um bloco delimitador.
Especificação em instruções rotuladas:
O escopo de um rótulo é todo o bloco no qual o rótulo é declarado, incluindo todos os blocos aninhados. É um erro de tempo de compilação que dois rótulos com o mesmo nome tenham escopos sobrepostos.
Um rótulo pode ser referenciado a partir de instruções goto (§13.10.4) no escopo do rótulo.
rótulos aninhados
Deve a: b: while (true) continue a; ter suporte?
Recomendação: não. Nenhum usuário solicitou isso. Nenhum caso de uso convincente foi apresentado para ele. A maioria dos idiomas tradicionais não permite, sem reclamações de suas comunidades. O lang (e impl) são mais simples e claros trazem estrito que somente a instrução direta que contém rótulos de instrução rotulada o loop/switch.
Reuniões de design
TBD
C# feature specifications