If let e while let

1. O problema que if let resolve

Em Rust, o match é uma ferramenta poderosa para lidar com padrões, mas pode se tornar verboso quando você está interessado em apenas um único caso. Considere o tratamento de Option<T>:

let valor: Option<i32> = Some(42);

// Abordagem com match (verboso para um único padrão)
match valor {
    Some(x) => println!("Valor encontrado: {}", x),
    None => (), // boilerplate necessário
}

// Abordagem com if let (conciso e focado)
if let Some(x) = valor {
    println!("Valor encontrado: {}", x);
}

O if let reduz o boilerplate eliminando a necessidade de escrever um braço None vazio, permitindo que você foque exclusivamente no caso de sucesso. Essa simplificação torna o código mais legível e menos propenso a erros.

2. Sintaxe e funcionamento do if let

A sintaxe básica do if let é:

if let padrao = expressao {
    // bloco executado se a expressão corresponder ao padrão
}

O padrão em if let deve ser refutável — ou seja, deve ter a possibilidade de não corresponder. Padrões irrefutáveis (como let x = 5;) não podem ser usados porque sempre corresponderiam, tornando o if redundante.

Exemplo com Option:

let nome: Option<&str> = Some("Alice");

if let Some(nome_interno) = nome {
    println!("Olá, {}!", nome_interno);
}

Exemplo com Result:

let resultado: Result<i32, &str> = Ok(100);

if let Ok(valor) = resultado {
    println!("Operação bem-sucedida: {}", valor);
}

3. if let com else e encadeamento

Assim como um if tradicional, if let pode ter uma cláusula else:

let numero: Option<i32> = None;

if let Some(x) = numero {
    println!("Número: {}", x);
} else {
    println!("Nenhum número presente");
}

Você também pode encadear múltiplos padrões com else if let:

let dado: Result<i32, String> = Err("Erro crítico".to_string());

if let Ok(valor) = dado {
    println!("Sucesso: {}", valor);
} else if let Err(msg) = dado {
    println!("Falha: {}", msg);
}

Limitação importante: Diferente do match, o if let não verifica exaustividade. Se você tiver muitos padrões, um match ainda é a escolha mais segura.

4. while let: repetição baseada em padrão

O while let combina a ideia de padrão refutável com um loop: ele executa o bloco enquanto a expressão corresponder ao padrão.

Sintaxe:

while let padrao = expressao {
    // bloco executado enquanto houver correspondência
}

Exemplo clássico com iteradores:

let mut pilha = vec![1, 2, 3, 4, 5];

while let Some(topo) = pilha.pop() {
    println!("Removido: {}", topo);
}
// Saída: Removido: 5, 4, 3, 2, 1

Comparação com loop + match:

let mut iter = (1..=3).into_iter();

// Abordagem com loop + match
loop {
    match iter.next() {
        Some(valor) => println!("{}", valor),
        None => break,
    }
}

// Equivalente com while let (mais limpo)
let mut iter = (1..=3).into_iter();
while let Some(valor) = iter.next() {
    println!("{}", valor);
}

5. Padrões avançados com if let e while let

Ambos os construtos suportam padrões complexos, incluindo desestruturação e guards.

Desestruturação de tuplas:

let ponto = (3, 5);

if let (x, 5) = ponto {
    println!("O ponto está na linha y=5, x={}", x);
}

Desestruturação de structs:

struct Pessoa {
    nome: String,
    idade: u8,
}

let pessoa = Pessoa {
    nome: "Carlos".to_string(),
    idade: 30,
};

if let Pessoa { nome, idade: 30 } = pessoa {
    println!("{} tem 30 anos", nome);
}

Uso de guards (if dentro do padrão):

let numero = Some(42);

if let Some(x) = numero if x > 30 {
    println!("Número grande: {}", x);
}

Combinando com @ bindings e intervalos:

let valor = Some(25);

if let Some(x @ 10..=50) = valor {
    println!("Valor entre 10 e 50: {}", x);
}

6. let else: alternativa moderna (Rust 1.65+)

Desde o Rust 1.65, você pode usar let else como uma alternativa elegante para if let quando precisa de early return ou tratamento de erro:

fn processar_id(id: Option<i32>) -> i32 {
    let Some(valor) = id else {
        eprintln!("ID ausente!");
        return -1;
    };
    // Aqui, 'valor' está disponível no escopo externo
    valor * 2
}

println!("{}", processar_id(Some(10))); // 20
println!("{}", processar_id(None));      // -1

Quando preferir let else ao if let:

  • Quando você precisa interromper o fluxo (return, break, continue) no caso negativo
  • Quando quer evitar aninhamento profundo de if let
  • Quando o valor extraído precisa estar disponível no escopo externo

Exemplo com validação:

fn validar_usuario(nome: Option<&str>, idade: Option<u8>) -> String {
    let Some(n) = nome else {
        return "Nome inválido".to_string();
    };
    let Some(i) = idade else {
        return "Idade inválida".to_string();
    };
    format!("Usuário: {}, Idade: {}", n, i)
}

7. Boas práticas e armadilhas comuns

Evitar if let aninhados em excesso:

// Ruim: aninhamento excessivo
if let Some(a) = x {
    if let Some(b) = a {
        if let Some(c) = b {
            println!("{}", c);
        }
    }
}

// Melhor: usar combinadores ou let else
if let Some(c) = x.and_then(|a| a).and_then(|b| b) {
    println!("{}", c);
}

Preferir match quando há muitos padrões:

// Ruim: if let para múltiplos padrões
if let 1 = valor {
    // ...
} else if let 2 = valor {
    // ...
} else if let 3 = valor {
    // ...
}

// Melhor: match para múltiplos padrões
match valor {
    1 => { /* ... */ }
    2 => { /* ... */ }
    3 => { /* ... */ }
    _ => { /* ... */ }
}

Cuidado com while let e loops infinitos acidentais:

let mut dado = Some(10);

// CUIDADO: loop infinito! O padrão sempre corresponde
while let Some(x) = dado {
    println!("{}", x);
    // 'dado' nunca é atualizado para None!
}

Legibilidade vs. concisão:

  • Use if let para casos simples com 1-2 padrões
  • Use match quando a lógica de padrões for complexa ou precisar de exaustividade
  • Use let else para validações com early return

Lembre-se: concisão não é o único objetivo. Um código ligeiramente mais verboso, porém mais claro, é quase sempre preferível.

Referências