Controle de Fluxo — if, loop, while e for como você nunca viu antes

Controle de Fluxo — if, loop, while e for como você nunca viu antes

Se o if produz valor, a construção que outras linguagens resolvem com ternário fica natural. O artigo percorre if como expression, os três laços e quando cada um cabe, os rótulos que decidem de qual laço o break sai, e por que o for é o que elimina a aritmética de índice.
Rust

9 min de leitura

No artigo Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores, aprendemos que Rust distingue statements de expressions, e que a ausência de ponto e vírgula no final de um bloco significa retorno de valor. Hoje esse conceito vai se expandir de forma elegante — porque em Rust, as próprias estruturas de controle de fluxo são expressions. Isso muda tudo.

O if que você conhece — e o que você não conhece

A forma básica do if em Rust é familiar:

fn main() {
    let temperatura = 38.5;

    if temperatura > 37.5 {
        println!("Febre detectada.");
    } else if temperatura > 36.0 {
        println!("Temperatura normal.");
    } else {
        println!("Abaixo do normal.");
    }
}

Até aqui, nada surpreendente. Mas há dois detalhes importantes que separam o if de Rust do de outras linguagens:

Primeiro: não há parênteses em torno da condição. Em C, Java ou JavaScript você escreve if (x > 0). Em Rust, os parênteses são desnecessários e o compilador inclusive avisa se você os usar sem necessidade.

Segundo: a condição deve ser um booleano. Sem exceções. Em C você pode escrever if (1) ou if (ponteiro). Em Rust, isso é um erro de compilação. A condição tem que ser explicitamente true ou false. Isso elimina uma classe inteira de bugs sutis.

if como expression — a grande virada

Aqui está o que torna o if de Rust especial. Como ele é uma expression, pode produzir um valor:

fn main() {
    let aprovado = true;

    let mensagem = if aprovado {
        "Parabéns, você passou!"
    } else {
        "Tente novamente."
    };

    println!("{mensagem}");
}

O if inteiro avalia para um valor, que é atribuído a mensagem. Isso substitui o operador ternário ? : de outras linguagens — de forma mais legível e mais poderosa.

Mas há uma regra importante: ambos os ramos devem retornar o mesmo tipo. O compilador não aceita isso:

fn main() {
    let x = 5;
    let y = if x > 0 { 42 } else { "negativo" }; // ERRO de tipo!
}

Um ramo retorna i32, o outro retorna &str. Rust recusa. Os tipos precisam ser compatíveis — e isso é verificado em tempo de compilação, não em execução.

loop — repetição incondicional

O loop em Rust é o laço mais simples: repete para sempre até que você peça para parar com break:

fn main() {
    let mut tentativas = 0;

    loop {
        tentativas += 1;
        println!("Tentativa {tentativas}");

        if tentativas == 3 {
            println!("Limite atingido.");
            break;
        }
    }
}

Saída:

Tentativa 1
Tentativa 2
Tentativa 3
Limite atingido.

Mas o loop também é uma expression. Você pode retornar um valor do break:

fn main() {
    let mut contador = 0;

    let resultado = loop {
        contador += 1;
        if contador == 10 {
            break contador * 2; // retorna 20
        }
    };

    println!("Resultado: {resultado}"); // 20
}

Isso é especialmente útil quando você precisa tentar uma operação repetidamente até obter um resultado válido — como ler entrada do usuário ou aguardar uma conexão.

Labels em loops aninhados

Quando você tem loops aninhados, o break por padrão afeta apenas o loop mais interno. Para sair de um loop externo, Rust oferece labels:

fn main() {
    let mut encontrado = false;

    'externo: for i in 0..5 {
        for j in 0..5 {
            if i + j == 6 {
                println!("Encontrado: i={i}, j={j}");
                encontrado = true;
                break 'externo; // sai do loop externo
            }
        }
    }

    if encontrado {
        println!("Busca concluída.");
    }
}

Labels começam com apóstrofo (') — uma sintaxe incomum, mas fácil de identificar visualmente. O continue também aceita labels para pular para a próxima iteração de um loop específico.

while — repetição condicional

O while é direto ao ponto: repete enquanto a condição for verdadeira:

fn main() {
    let mut n = 1;

    while n < 100 {
        n *= 2;
    }

    println!("Primeira potência de 2 acima de 100: {n}");
}

Saída:

Primeira potência de 2 acima de 100: 128

Um padrão muito comum em Rust é o while let, que combina o laço com pattern matching — veremos mais sobre isso nos artigos de enums. Por enquanto, um vislumbre:

fn main() {
    let mut pilha = vec![1, 2, 3, 4, 5];

    while let Some(topo) = pilha.pop() {
        println!("Removido: {topo}");
    }

    println!("Pilha vazia.");
}

Saída:

Removido: 5
Removido: 4
Removido: 3
Removido: 2
Removido: 1
Pilha vazia.

O loop continua enquanto pilha.pop() retornar Some(valor), e para automaticamente quando retornar None — ou seja, quando a pilha esvaziar. Elegante e seguro.

for — o laço mais usado em Rust

O for em Rust não opera sobre índices como em C. Ele itera sobre iteradores — e isso o torna ao mesmo tempo mais seguro e mais expressivo:

fn main() {
    let frutas = ["maçã", "banana", "laranja", "uva"];

    for fruta in frutas {
        println!("Fruta: {fruta}");
    }
}

Não há risco de acessar um índice inválido. Não há off-by-one errors. O for consome cada elemento do iterador na ordem correta e para sozinho.

Ranges

Para iterar sobre números, Rust usa ranges:

fn main() {
    // 1 até 5, incluindo o 5
    for i in 1..=5 {
        println!("{i}");
    }

    // 0 até 4, excluindo o 5
    for i in 0..5 {
        println!("{i}");
    }
}

1..=5 é um range inclusivo — inclui o valor final. 0..5 é um range exclusivo — vai de 0 a 4. Essa distinção é explícita na sintaxe, o que elimina ambiguidade.

Iterando com índice

Quando você precisa do índice junto com o valor, use .enumerate():

fn main() {
    let linguagens = ["Rust", "Go", "C", "Python"];

    for (indice, linguagem) in linguagens.iter().enumerate() {
        println!("{}. {}", indice + 1, linguagem);
    }
}

Saída:

1. Rust
2. Go
3. C
4. Python

Um programa completo: tabuada

Vamos unir tudo que aprendemos em um programa que gera tabuadas:

fn tabuada(numero: u32) {
    println!("\n── Tabuada do {} ──", numero);

    for i in 1..=10 {
        let resultado = numero * i;
        let linha = if i == 10 {
            format!("{numero} × {i:2} = {resultado}")
        } else {
            format!("{numero} × {i:2} = {resultado}")
        };
        println!("{linha}");
    }
}

fn main() {
    for n in 1..=5 {
        tabuada(n);
    }
}

Saída (primeiras linhas):

── Tabuada do 1 ──
1 × 1 = 1
1 × 2 = 2
...
── Tabuada do 2 ──
2 × 1 = 2
2 × 2 = 4
...

O que você deve guardar deste artigo

Três ideias centrais:

Primeira: if, loop e blocos são expressions em Rust. Eles produzem valores. Isso elimina operadores ternários, variáveis temporárias desnecessárias e torna o código mais direto.

Segunda: tipos devem ser consistentes. Se um if retorna valor, todos os ramos retornam o mesmo tipo. O compilador garante isso antes de qualquer execução.

Terceira: o for em Rust não é um laço de índices — é um laço de iteradores. Isso é fundamental para entender Rust, pois iteradores permeiam toda a linguagem e serão um dos temas mais ricos da série.

Exercícios

Exercício 1

Reescreva o trecho abaixo usando if como expression, sem mut.

let mut taxa = 0.0;
if valor > 1000.0 {
    taxa = 0.02;
} else {
    taxa = 0.05;
}
Ver resposta

✓ Resposta:

let taxa = if valor > 1000.0 { 0.02 } else { 0.05 };

Além de mais curto, o resultado é imutável: o valor é decidido de uma vez e não pode ser alterado depois por engano. É o padrão idiomático em Rust — o mut ali existia só porque a linguagem de origem não tinha if como expression.

Exercício 2

Explique a diferença entre loop, while e for, e diga qual deles é o único capaz de devolver um valor.

Ver resposta

✓ Resposta: loop repete indefinidamente até um break; while testa uma condição antes de cada repetição; for percorre um iterador, do primeiro ao último elemento. Apenas o loop devolve valor, através do break:

let primeiro_multiplo = loop {
    contador += 1;
    if contador % 7 == 0 {
        break contador;
    }
};

O for é o mais usado porque elimina a aritmética de índice, que é onde moram os erros de um a mais ou um a menos.

Exercício 3

O laço abaixo tem um bug. Identifique-o e corrija usando for.

let nomes = ["Ana", "Bruno", "Carla"];
let mut i = 0;
while i <= nomes.len() {
    println!("{}", nomes[i]);
    i += 1;
}
Ver resposta

✓ Resposta: A condição usa <= em vez de <, então o laço tenta acessar nomes[3] num array de três elementos. Rust não lê memória inválida: verifica o limite em tempo de execução e encerra o programa com index out of bounds: the len is 3 but the index is 3 — um panic, não uma falha silenciosa como seria em C. A correção idiomática elimina o índice:

for nome in nomes {
    println!("{nome}");
}

Assim o erro deixa de ser possível: não há condição de parada para escrever errado.

Exercício 4

Escreva um laço que percorra os números de 1 a 20 e pare no primeiro divisível por 13, guardando-o numa variável. Explique a escolha da construção.

Ver resposta

✓ Resposta:

fn main() {
    let mut achado = None;
    for n in 1..=20 {
        if n % 13 == 0 {
            achado = Some(n);
            break;
        }
    }
    println!("{achado:?}");   // Some(13)
}

O for com o intervalo inclusivo 1..=20 evita escrever a condição de parada à mão. O Option cobre o caso de não haver nenhum divisível no intervalo, sem inventar um valor mágico como -1 para significar ausência.

Exercício 5

Explique o que fazem os rótulos em laços aninhados e por que eles são preferíveis a uma variável de controle.

Ver resposta

✓ Resposta: Um rótulo nomeia o laço, permitindo que break e continue escolham a qual deles se aplicam — sem ele, ambos afetam apenas o laço mais interno.

'externo: for i in 0..5 {
    for j in 0..5 {
        if i * j > 6 {
            break 'externo;
        }
    }
}

A alternativa sem rótulo é uma variável booleana testada nos dois laços, que espalha a lógica de saída por vários pontos e é fácil de esquecer em um deles. O rótulo diz exatamente onde o fluxo vai parar, no ponto em que a decisão é tomada.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas
Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas

O que torna o distribuído difícil não é a concorrência, é não conseguir…

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Rust distingue o que executa do que produz valor, e essa distinção decide como…

Programação de Sistemas — Arquivos, Processos e Sinais
Programação de Sistemas — Arquivos, Processos e Sinais

Descer ao nível do sistema operacional traz de volta arquivos, processos e…