Fluxo de Controle: switch, goto e Saltos com Rótulo
Objective
O C# tem quatro formas de sair da linha de execução atual sem retornar: break,
continue, goto e (dentro de um switch statement) goto case. Este concept
cobre o statement switch e a regra de não haver fall-through, goto case e
goto default, goto com rótulo, e o break e continue com rótulo do C# 15,
que deixam um statement aninhado sair de um laço externo, ou continuá-lo, sem
variável de flag nem goto. break e continue com rótulo são uma feature
estável no C# 15 (SDK 11.0.100, sem precisar de LangVersion preview). A
expressão switch e os patterns estão em type-constant-and-relational-patterns;
aqui o assunto é o statement e os saltos. Todo código de erro e saída abaixo foi
compilado no SDK 11.0.100.
Use Cases
- Sair de dois ou três laços aninhados assim que uma busca tem sucesso.
- Pular para a próxima linha (ou o próximo arquivo, ou o próximo registro) de dentro de um laço interno.
- Um
switchem que um case continua deliberadamente em outro, por exemplo níveis ou permissões que incluem os de baixo. - Um laço
whilecujo corpo é umswitchque precisa conseguir continuar ou parar o laço, e não só o switch. - Ler comentários de code review como "troque essa flag por um rótulo" e decidir se o rótulo é mais claro.
Deep Dive
O switch statement e o fim do fall-through
Em um switch statement, toda seção precisa terminar em um statement que não
alcance o próximo rótulo: break, return, throw, continue ou um goto. Se
uma seção cai pelo fim, o compilador recusa:
csharpswitch (n)
{
case 1:
Console.WriteLine("one");
case 2: // error CS0163: Control cannot fall through from one case label ('case 1:')
Console.WriteLine("two");
break;
}A última seção tem a mesma regra, reportada como CS8070. Empilhar rótulos sem
statements entre eles é permitido, porque não há código para cair:
csharpcase 0:
case -1:
text = "none";
break;Outras checagens vêm da mesma análise: uma constante repetida é CS0152, e um
goto case 5; para um rótulo que não existe é CS0159: No such label 'case 5:' within the scope of the goto statement. Uma cláusula when funciona também no
statement (case int n when n > 10:), e um case pode ser um pattern, então o
switch statement compartilha a sintaxe de patterns com a expressão.
goto case e goto default
Quando você quer a continuação estilo C, você diz isso:
csharpstatic string Tier(int n)
{
string r = "";
switch (n)
{
case 3: r += "gold "; goto case 2;
case 2: r += "silver "; goto case 1;
case 1: r += "bronze"; break;
case 0:
case -1: r = "none"; break;
default: r = "?"; break;
}
return r;
}
// 3: "gold silver bronze", 2: "silver bronze", 1: "bronze", 0: "none", 9: "?"goto default; salta para a seção default, útil quando um case com pattern
(case int:) decide que, no fim, não trata a entrada. Com F(50), F(1),
F("x") e F(2.5) sobre um switch com case int n when n > 10,
case int: goto default;, case string s e default, os resultados são big,
other, s:x e other. Quando a continuação é na verdade uma consulta, uma
expressão switch costuma ler melhor, porque não tem saltos:
k switch { 3 => "gold silver bronze", 2 => "silver bronze", _ => "?" }.
goto com rótulo
goto label; salta para um statement com rótulo no mesmo método, no mesmo bloco
ou em um bloco que o envolve. Não dá para saltar para dentro de um bloco:
csharpgoto inside;
{
inside: Console.WriteLine(); // error CS0159: No such label 'inside'
}Um rótulo nunca usado é CS0164 (warning), um duplicado é CS0140 e um ausente é
CS0159. Os usos legítimos clássicos são pequenos: um retry escrito como salto
para trás (um laço costuma ser mais claro) e, antes do C# 15, sair de laços
aninhados:
csharpint found = -1;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
if (i * j == 2) { found = i * 10 + j; goto done; }
done:
Console.WriteLine(found); // 12Esse é o caso que o break com rótulo substituiu na maior parte do código.
Prefira-o agora, a menos que o salto vá para algo que não é a saída de um laço.
break e continue com rótulo (C# 15)
Um rótulo antes de um laço (for, foreach, while) ou de um switch pode ser
alvo de break ou continue de qualquer ponto dentro dele:
csharpint visited = 0;
int[][] grid = [[1, 0, 1], [2, 3, 9], [4, 5, 6]];
outer:
for (int r = 0; r < grid.Length; r++)
{
for (int c = 0; c < grid[r].Length; c++)
{
visited++;
if (grid[r][c] == 0) continue outer; // próxima linha
if (grid[r][c] == 3) break outer; // sai dos dois laços
}
}
// visited é 4foreach funciona igual (um continue rows; sobre foreach aninhados contou 8
células na mesma grade, porque cada 0 pula o resto da sua linha). Um switch
também pode ter rótulo, o que importa dentro de um laço, porque um break simples
em uma seção do switch só sai do switch:
csharpint i = 0;
loop:
while (i < 10)
{
i++;
sw:
switch (i)
{
case 2: continue loop; // próxima iteração do while
case 4: break sw; // sai do switch, depois imprime
case 6: break loop; // sai do while
default: break;
}
Console.WriteLine($"after switch i={i}");
}
// imprime para i = 1, 3, 4, 5; termina com i 6As regras, a partir dos erros que o compilador deu:
| Código | Caso |
|---|---|
CS9393 |
break nope; sem laço ou switch envolvente com esse rótulo |
CS9393 |
o rótulo está em um if ou bloco, não em um laço ou switch |
CS9393 |
o salto está dentro de uma função local e o rótulo fica fora dela |
CS1632 |
o salto está dentro de uma lambda: o controle não pode sair de um método anônimo |
CS0140 |
dois rótulos com o mesmo nome em um método |
CS0164 |
warning: um rótulo que nada referencia |
CS9327 |
o projeto está em LangVersion 14 |
É uma feature só de compilador, como o with(...) em
collection-expressions-spread-and-with-arguments: o IL emitido são desvios
comuns, então compila para frameworks alvo mais antigos com LangVersion 15 no
SDK 11. O panorama está em whats-new-in-csharp-15.
Escolhendo: rótulo, flag, função local ou exceção
O mesmo problema de "sair de dois laços" tem quatro soluções:
- Uma flag (
bool found) testada na condição de cada laço. Funciona em todo lugar, mas acrescenta estado, e um teste esquecido mantém o laço interno rodando. - Uma função local que faz
return. Muitas vezes a melhor: a busca vira um método com nome que devolve o resultado, sem salto nenhum (vejalocal-functions-lambdas-and-closures). - Um rótulo (
break outer;). Curto e visível, bom quando os laços estão em um método e a saída é um salto puro. - Uma exceção. Nunca para fluxo de controle no caminho normal (veja
exceptions-try-catch-filters-and-finally).
Um teste útil: se o código depois dos laços precisa da posição encontrada, devolva-a de um método. Se só precisa saber que o laço terminou, um rótulo basta.
Trade-offs
- Saltos escondem a forma do laço. Um
continue outer;três níveis abaixo é fácil de ler errado. Mantenha curtos os laços com rótulo, dê ao rótulo o nome do que ele itera (rows,files) e extraia a parte interna se ela crescer. - Um rótulo precisa ser usado. Um rótulo sem referência é
CS0164. Trate o warning como sinal de que um refactor deixou estrutura morta. - Rótulos param nas fronteiras de método. Uma lambda ou função local não
consegue saltar para um rótulo fora dela (
CS1632,CS9393), então mover o corpo de um laço para uma delas pode quebrar umcontinue outer. - Cadeias de
goto casecrescem. É legal e explícito, mas uma cadeia longa é uma máquina de estados disfarçada. Se os cases são valores a consultar, use uma expressãoswitch. - Exige o C# 15. Projetos no SDK 10 recebem
CS9327para saltos com rótulo. Até todo build estar no SDK 11, a flag ou a função local é a escolha portável. breakdentro deswitchnão ébreakdo laço. O bug mais comum em laços com switch é escreverbreakesperando que o laço pare. Coloque rótulo no laço.