break Instruções e continue rótulos

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.

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:

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 delimitanteswitch, while, doou foreachforinstruçãomais próxima determinada como acima. Se uma break instrução não estiver entre um switch, , while, doou forforeach instrução, ocorrerá um erro de tempo de compilação.Se não existir tal instrução delimitaçã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 break instrução sair de um ou mais try blocos com blocos associados finally , o controle será inicialmente transferido para o finally bloco da instrução mais try interna. Quando e se o controle atingir o ponto final de um finally bloco, o controle será transferido para o finally bloco da próxima instrução delimitadora try . Esse processo é repetido até que os finally blocos de todas as instruções intermediárias try tenham sido executados.
  • O controle é transferido para o destino da break instruçã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 é break resolvido 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 whiledoformais próxima, ou foreachiteration_statement determinada como acima. Se uma continue instrução não estiver entre um while, , doforou foreach uma instrução, ocorrerá um erro de tempo de compilação.Se não existir tal instrução delimitaçã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 continue instrução sair de um ou mais try blocos com blocos associados finally , o controle será inicialmente transferido para o finally bloco da instrução mais try interna. Quando e se o controle atingir o ponto final de um finally bloco, o controle será transferido para o finally bloco da próxima instrução delimitadora try . Esse processo é repetido até que os finally blocos de todas as instruções intermediárias try tenham sido executados.
  • O controle é transferido para o destino da continue instruçã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 é continue resolvido 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 por END_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.

Esta proposta consolida e aborda as seguintes discussões da comunidade:

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