Decidir e repetir são as duas habilidades que transformam uma lista de instruções num programa de verdade.
Até agora nossos programas rodavam de cima para baixo, uma vez só. Em "Operadores, Texto e Conversa com o Usuário" já usamos o if informalmente e escolhemos entre dois valores com o operador condicional. Hoje formalizamos o controle de fluxo: o if com todos os seus caminhos, o switch nas formas clássica e moderna, e os quatro laços do C#. No fim, a Livraria ganha um menu que só fecha quando o cliente manda.
if, else if e else
A estrutura é a que você já conhece de outras linguagens:
int quantidade = 7;
decimal percentual;
if (quantidade >= 10)
{
percentual = 0.15m;
}
else if (quantidade >= 3)
{
percentual = 0.10m;
}
else
{
percentual = 0m;
}
Console.WriteLine($"Desconto: {percentual:P0}"); // Desconto: 10%
As condições são testadas em ordem, e só o primeiro bloco verdadeiro executa. A ordem importa: se o teste >= 3 viesse primeiro, uma compra de 10 unidades cairia nele e ganharia só 10%.
Duas diferenças em relação ao C. A condição precisa ser um bool: if (quantidade) não compila, porque um inteiro não é verdadeiro nem falso em C#. Isso elimina de vez o clássico erro if (x = 5), que em C atribui em vez de comparar. E repare que percentual foi declarado sem valor e só recebeu valor dentro dos blocos. O compilador aceita porque consegue provar que todo caminho atribui um valor. Remova o else e ele acusa o uso de variável não atribuída.
As chaves são opcionais quando o bloco tem uma instrução só, mas o conselho é usá-las sempre em código de produção. Uma segunda linha adicionada depois, com a indentação enganando o leitor, é um bug que já custou caro a muita gente.
O switch clássico
Quando a decisão depende de um único valor comparado com várias possibilidades, o switch organiza melhor do que uma escada de else if:
int opcao = 2;
switch (opcao)
{
case 1:
Console.WriteLine("Listar catálogo");
break;
case 2:
Console.WriteLine("Fazer pedido");
break;
case 0:
Console.WriteLine("Sair");
break;
default:
Console.WriteLine("Opção inválida");
break;
}
Aqui o C# corrigiu uma das armadilhas mais famosas do C. Em C, esquecer o break faz a execução "cair" no caso seguinte. Em C#, isso é erro de compilação (CS0163): cada seção precisa terminar com break, return ou outra instrução que encerre o fluxo. Se você quer que vários valores levem ao mesmo código, empilhe os rótulos:
switch (opcao)
{
case 1:
case 2:
Console.WriteLine("Operação de catálogo");
break;
default:
Console.WriteLine("Outra coisa");
break;
}
O switch como expressão
O C# 8 trouxe uma forma mais compacta e poderosa: a expressão switch, que produz um valor. Compare com o if de desconto do início:
decimal percentual = quantidade switch
{
>= 10 => 0.15m,
>= 3 => 0.10m,
_ => 0m
};
Leia assim: "conforme a quantidade: se for maior ou igual a 10, 15%; se for maior ou igual a 3, 10%; em qualquer outro caso, zero". Cada linha é um braço com um padrão à esquerda da seta e um resultado à direita. O _ é o descarte, que casa com qualquer coisa e faz o papel do default.
Os padrões podem ser combinados com and, or e not:
int paginas = 256;
string categoria = paginas switch
{
<= 0 => "inválido",
< 100 => "leitura rápida",
>= 100 and < 400 => "leitura média",
_ => "calhamaço"
};
Console.WriteLine(categoria); // leitura média
A expressão switch precisa cobrir todos os casos possíveis. Se você esquecer o _ e sobrar algum valor sem braço, o compilador emite o aviso CS8509. Se esse valor aparecer durante a execução, o programa lança uma SwitchExpressionException. Leve o aviso a sério.
Quando usar cada forma? A expressão switch é ideal para calcular um valor a partir de outro. O switch clássico e o if servem melhor quando cada caminho executa ações, com várias linhas. Os padrões ficam ainda mais poderosos com tipos e objetos, assunto de uma aula inteira na Fase 2.
while e do-while
O while repete um bloco enquanto a condição for verdadeira, testando antes de cada volta:
int estoque = 3;
while (estoque > 0)
{
Console.WriteLine($"Vendido! Restam {estoque - 1}.");
estoque--;
}
Console.WriteLine("Estoque esgotado.");
O do-while testa depois, e por isso executa o bloco pelo menos uma vez. É a estrutura natural para menus e para perguntar até receber uma resposta válida:
int idade;
do
{
Console.Write("Idade (0 a 120): ");
}
while (!int.TryParse(Console.ReadLine(), out idade) || idade < 0 || idade > 120);
Console.WriteLine($"Idade registrada: {idade}");
O programa pergunta, testa e, se a resposta não servir, pergunta de novo. Repare que toda a validação cabe na condição, graças ao curto-circuito do || visto na aula anterior: os testes de faixa só acontecem se a conversão deu certo.
for
Quando você sabe quantas vezes vai repetir, o for junta numa linha a inicialização, a condição e o passo:
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"Linha {i}");
}
As três partes são: começa com i = 1; repete enquanto i <= 5; ao fim de cada volta, faz i++. A variável i só existe dentro do laço. Tentar usá-la depois é erro de compilação.
Laços podem ser aninhados. A tabuada é o exemplo clássico:
for (int linha = 1; linha <= 3; linha++)
{
for (int coluna = 1; coluna <= 5; coluna++)
{
Console.Write($"{linha * coluna,4}");
}
Console.WriteLine();
}
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
foreach
O foreach percorre cada elemento de uma coleção, sem índice nenhum para controlar:
string titulo = "Dom Casmurro";
int vogais = 0;
foreach (char c in titulo.ToLower())
{
if ("aeiou".Contains(c))
vogais++;
}
Console.WriteLine($"\"{titulo}\" tem {vogais} vogais."); // "Dom Casmurro" tem 4 vogais.
Uma string é uma sequência de caracteres, então o foreach passa por cada um. O mesmo vale para arrays, como o int[] que vimos de relance em "Cada Valor Tem um Tipo":
decimal[] precos = [59.90m, 42.50m, 38.00m];
decimal soma = 0;
foreach (decimal preco in precos)
soma += preco;
Console.WriteLine($"Soma: {soma}"); // Soma: 140,40
Arrays e as demais coleções ganham uma aula própria logo adiante. O foreach será o laço que você mais vai usar em C#: sempre que não precisar do índice, prefira-o ao for. Ele elimina os erros de "um a mais" e "um a menos" nos limites.
break e continue
Dentro de qualquer laço, break sai imediatamente e continue pula para a próxima volta:
for (int i = 1; i <= 20; i++)
{
if (i % 2 == 0)
continue; // ignora os pares
if (i > 9)
break; // para de vez ao passar de 9
Console.Write($"{i} ");
}
// 1 3 5 7 9
Um padrão comum é o laço "infinito" controlado por break: while (true) { ... if (condicaoDeSaida) break; ... }. É legítimo quando a condição de saída fica mais clara no meio do bloco do que no topo, mas não abuse. Se a saída puder ir para a condição do laço, ela deve ir.
Juntando tudo: o menu da Livraria
Agora a Livraria tem um menu que se repete até o cliente escolher sair:
using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("pt-BR");
int opcao;
do
{
Console.WriteLine();
Console.WriteLine("=== Livraria ===");
Console.WriteLine("1 - Ver catálogo");
Console.WriteLine("2 - Fazer pedido");
Console.WriteLine("0 - Sair");
Console.Write("Escolha: ");
if (!int.TryParse(Console.ReadLine(), out opcao))
opcao = -1; // força a opção inválida
switch (opcao)
{
case 1:
Console.WriteLine($"{"Dom Casmurro",-25}{59.90m,12:C}");
Console.WriteLine($"{"O Cortiço",-25}{42.50m,12:C}");
Console.WriteLine($"{"Vidas Secas",-25}{38.00m,12:C}");
break;
case 2:
Console.Write("Preço unitário: ");
decimal.TryParse(Console.ReadLine(), out decimal preco);
Console.Write("Quantidade: ");
int.TryParse(Console.ReadLine(), out int quantidade);
if (preco <= 0 || quantidade <= 0)
{
Console.WriteLine("Pedido inválido.");
break;
}
decimal subtotal = preco * quantidade;
decimal percentual = quantidade switch
{
>= 10 => 0.15m,
>= 3 => 0.10m,
_ => 0m
};
decimal total = subtotal * (1 - percentual);
Console.WriteLine($"Total com {percentual:P0} de desconto: {total:C}");
break;
case 0:
Console.WriteLine("Até logo!");
break;
default:
Console.WriteLine("Opção inválida.");
break;
}
}
while (opcao != 0);
Um detalhe honesto sobre a opção 2: aqui ignoramos o bool devolvido pelo TryParse e confiamos no valor zero que ele deixa quando a conversão falha. O teste preco <= 0 || quantidade <= 0 pega esse caso. Funciona, mas esconde a intenção. Um leitor pode achar que esquecemos de verificar. Na próxima aula vamos encapsular "perguntar até receber um número válido" num lugar só e reaproveitá-lo em todo o programa.
Repare também que o break dentro do if sai do switch, e não do do-while. O break sempre encerra a estrutura mais interna entre switch, for, foreach, while e do.
Fontes e leituras recomendadas
- Instruções de seleção:
ifeswitch(Microsoft Learn): https://learn.microsoft.com/dotnet/csharp/language-reference/statements/selection-statements. É a referência completa, com as regras doswitchclássico. - Expressão switch: https://learn.microsoft.com/dotnet/csharp/language-reference/operators/switch-expression. Traz a sintaxe, as regras de exaustividade e exemplos.
- Padrões (patterns): https://learn.microsoft.com/dotnet/csharp/language-reference/operators/patterns. Cobre padrões relacionais, lógicos e os demais, que serão aprofundados na Fase 2.
- Instruções de iteração: https://learn.microsoft.com/dotnet/csharp/language-reference/statements/iteration-statements. Mostra
for,foreach,whileedolado a lado. Random.Shared(referência da API): https://learn.microsoft.com/dotnet/api/system.random.shared. É o gerador de números aleatórios usado no exercício 5.
Exercícios
Exercício 1
Leia a idade de uma pessoa e classifique-a com if/else if/else em: "criança" (0 a 11), "adolescente" (12 a 17), "adulto" (18 a 59) ou "idoso" (60 ou mais). Idades negativas devem ser recusadas.
Ver resposta
✓ Resposta:
Console.Write("Idade: ");
if (!int.TryParse(Console.ReadLine(), out int idade) || idade < 0)
{
Console.WriteLine("Idade inválida.");
return;
}
string faixa;
if (idade <= 11)
faixa = "criança";
else if (idade <= 17)
faixa = "adolescente";
else if (idade <= 59)
faixa = "adulto";
else
faixa = "idoso";
Console.WriteLine($"Faixa: {faixa}");
Como as condições são testadas em ordem, cada uma só precisa verificar o limite superior: se chegou ao teste idade <= 17, já se sabe que a idade é maior que 11. Repare no return; sem valor: nas instruções de nível superior ele encerra o programa. Aqui não devolvemos código de saída porque nenhum outro return do arquivo devolve valor, e o compilador exige consistência.
Exercício 2
Reescreva a classificação do exercício anterior usando uma expressão switch com padrões relacionais.
Ver resposta
✓ Resposta:
string faixa = idade switch
{
< 0 => "inválida",
<= 11 => "criança",
<= 17 => "adolescente",
<= 59 => "adulto",
_ => "idoso"
};
A expressão switch também testa os braços em ordem, então vale a mesma lógica do if: cada braço só precisa do limite superior. O braço < 0 torna a expressão autossuficiente, sem depender de uma validação anterior. Esta versão é mais curta e deixa explícito que o objetivo é calcular um valor, que é exatamente o caso de uso da expressão switch.
Exercício 3
Escreva o clássico FizzBuzz de 1 a 30: para múltiplos de 3, imprima "Fizz"; de 5, "Buzz"; de ambos, "FizzBuzz"; nos demais, o próprio número.
Ver resposta
✓ Resposta:
for (int i = 1; i <= 30; i++)
{
if (i % 15 == 0)
Console.WriteLine("FizzBuzz");
else if (i % 3 == 0)
Console.WriteLine("Fizz");
else if (i % 5 == 0)
Console.WriteLine("Buzz");
else
Console.WriteLine(i);
}
A armadilha do exercício é a ordem. O teste de "múltiplo de 3 e de 5" precisa vir primeiro. Se i % 3 == 0 viesse antes, o 15 cairia nele e imprimiria só "Fizz". Testar i % 15 == 0 funciona porque 3 e 5 não têm fatores em comum, então ser múltiplo dos dois equivale a ser múltiplo de 15. A forma i % 3 == 0 && i % 5 == 0 é equivalente e talvez mais clara para quem lê.
Exercício 4
Leia um número inteiro positivo e calcule a soma dos seus dígitos usando while. Para 4096, o resultado é 19.
Ver resposta
✓ Resposta:
Console.Write("Número positivo: ");
if (!int.TryParse(Console.ReadLine(), out int n) || n <= 0)
{
Console.WriteLine("Número inválido.");
return;
}
int original = n;
int soma = 0;
while (n > 0)
{
soma += n % 10; // o último dígito
n /= 10; // remove o último dígito (divisão inteira)
}
Console.WriteLine($"Soma dos dígitos de {original}: {soma}");
O exercício reúne dois conceitos da aula anterior: % 10 dá o último dígito, e a divisão inteira por 10 remove esse dígito, porque descarta a parte fracionária. Para 4096: 6 + 9 + 0 + 4 = 19. O laço termina quando n chega a zero. Guardamos o valor em original porque o laço destrói n durante as contas.
Exercício 5
Escreva um jogo de adivinhação: o programa sorteia um número de 1 a 100, o jogador chuta até acertar, recebendo "maior" ou "menor" a cada tentativa. No fim, mostre quantas tentativas foram necessárias. Use Random.Shared.Next(1, 101) para sortear.
Ver resposta
✓ Resposta:
int secreto = Random.Shared.Next(1, 101); // de 1 a 100: o limite superior é exclusivo
int tentativas = 0;
int chute;
do
{
Console.Write("Seu chute (1 a 100): ");
if (!int.TryParse(Console.ReadLine(), out chute) || chute < 1 || chute > 100)
{
Console.WriteLine("Digite um número de 1 a 100.");
continue;
}
tentativas++;
if (chute < secreto)
Console.WriteLine("O número secreto é maior.");
else if (chute > secreto)
Console.WriteLine("O número secreto é menor.");
}
while (chute != secreto);
Console.WriteLine($"Acertou em {tentativas} tentativa(s)!");
Três pontos. Primeiro, Next(1, 101) inclui o 1 e exclui o 101, uma convenção comum nas APIs do .NET. Por isso usamos 101 para ter 100 como possibilidade. Segundo, o continue num do-while salta para o teste da condição. Como um chute inválido não é igual ao secreto, o laço segue normalmente, e só as tentativas válidas são contadas. Terceiro, com a estratégia de sempre chutar o meio do intervalo restante (a busca binária), qualquer número de 1 a 100 é encontrado em no máximo 7 tentativas, porque 2⁷ = 128 > 100. Guarde essa ideia: ela reaparece quando estudarmos busca em coleções e índices de banco de dados.