O Programa Que Escolhe e Repete: if, switch e Laços

O Programa Que Escolhe e Repete: if, switch e Laços

Controle de fluxo em C#: if, else if e else, o switch clássico que não deixa cair no caso seguinte, a expressão switch com padrões relacionais e a exaustividade, while, do-while, for, foreach, break e continue, e o menu da Livraria que se repete até o cliente sair.
Linguagem C#

• • 12 min de leitura

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

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.

Comentários

Mais em Linguagem C#

Configuração, logging e testes de integração: a API pronta para ser operada
Configuração, logging e testes de integração: a API pronta para ser operada

A API da Livraria pronta para operar: configuração em camadas com o padrão…

Performance e distribuição: BenchmarkDotNet, Native AOT e ferramentas de linha de comando
Performance e distribuição: BenchmarkDotNet, Native AOT e ferramentas de linha de comando

Medir antes de otimizar com BenchmarkDotNet e as ferramentas de diagnóstico do…

Do Console ao Jogo Online: o Mapa da Jornada em C#
Do Console ao Jogo Online: o Mapa da Jornada em C#

O mapa do curso de C#: setenta e três aulas em onze fases, dos fundamentos e…