Em Rust, os padrões (patterns) são uma ferramenta poderosa para desestruturar dados e controlar o fluxo do programa. Embora o match seja completo e flexível, em situações onde nos importamos com apenas um ou alguns padrões, ele pode se tornar verboso. Para esses casos, Rust oferece os constructos if let e while let, que são açúcares sintáticos que simplificam o código, mantendo a segurança e a expressividade.

Nesta aula, vamos entender como esses constructos funcionam, quando são apropriados e como combiná-los com else para lidar com casos não correspondentes. Veremos exemplos práticos que demonstram sua utilidade no dia a dia da programação Rust.

Açúcar para match

O if let é uma forma concisa de escrever um match que corresponde a apenas um padrão e ignora todos os outros. Em vez de escrever um bloco match com um braço e um curinga (_), podemos usar if let para tornar a intenção mais clara e o código mais enxuto.

Considere o exemplo com Option<T>. Sem if let, para verificar se um valor é Some e extrair o valor interno, faríamos:

let valor = Some(42);

match valor {
    Some(x) => println!("O valor é {}", x),
    None => (), // ignoramos o caso None
}

Com if let, o mesmo código fica mais direto:

if let Some(x) = valor {
    println!("O valor é {}", x);
}

A sintaxe geral é:

if let padrao = expressao {
    // código se o padrão corresponde
}

O while let funciona de forma análoga, mas em um loop: enquanto o padrão corresponder, o bloco é executado. É útil para consumir iteradores ou processar dados até que uma condição de falha ocorra.

Por exemplo, para percorrer uma Vec<Option<i32>> e extrair apenas os valores Some:

let mut valores = vec![Some(1), None, Some(2), None];

while let Some(Some(x)) = valores.pop() {
    println!("Encontrado: {}", x);
}

Aqui, valores.pop() retorna Option<Option<i32>>. O padrão Some(Some(x)) corresponde apenas se houver um elemento e ele for Some. O loop continua enquanto isso for verdade.

Quando usar

Use if let quando você está interessado em apenas uma variante de um enum ou em um único padrão, e deseja ignorar todos os outros. É especialmente útil para tipos como Option<T> e Result<T, E>, onde normalmente nos importamos com o caso de sucesso e ignoramos o erro (ou vice-versa).

Evite usar if let quando você precisa lidar com múltiplos padrões distintos. Nesse caso, match é mais adequado, pois permite tratar cada braço explicitamente. Além disso, se você precisa de uma cláusula else que execute código quando o padrão não corresponde, if let com else é uma boa opção.

Quanto ao while let, é ideal para loops onde a condição de continuação é baseada em um padrão. Por exemplo, ao consumir um iterador que retorna Option (como em next()), ou ao processar mensagens de um canal.

Em geral, prefira if let e while let quando eles tornarem o código mais legível e conciso. Se o match for mais claro, use-o.

Combinando com else

O if let pode ser combinado com um else para executar código quando o padrão não corresponde. Isso é equivalente a um match com um braço curinga que executa uma ação.

Exemplo com Result:

let resultado: Result<i32, &str> = Err("falha");

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

Isso é equivalente a:

match resultado {
    Ok(valor) => println!("Sucesso: {}", valor),
    Err(_) => println!("Falha"),
}

O while let não possui uma cláusula else intrínseca, mas você pode usar um if dentro do loop para verificar condições adicionais, ou simplesmente deixar o loop terminar quando o padrão não corresponder.

Também é possível encadear múltiplos if let com else if, embora isso possa se tornar confuso. Nesses casos, considere usar match.

Exemplos

Vamos ver alguns exemplos práticos que ilustram o uso de if let e while let.

Exemplo 1: Extraindo valor de Option

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

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

Exemplo 2: Tratando erro com Result

let arquivo: Result<String, std::io::Error> = std::fs::read_to_string("config.txt");

if let Ok(conteudo) = arquivo {
    println!("Conteúdo: {}", conteudo);
} else {
    println!("Não foi possível ler o arquivo");
}

Exemplo 3: Loop com while let em um iterador

let mut iter = (0..5).into_iter();

while let Some(x) = iter.next() {
    println!("Número: {}", x);
}

Exemplo 4: Desestruturando um enum personalizado

enum Mensagem {
    Texto(String),
    Sair,
}

fn processar(msg: Mensagem) {
    if let Mensagem::Texto(t) = msg {
        println!("Mensagem: {}", t);
    } else {
        println!("Comando de saída");
    }
}

fn main() {
    processar(Mensagem::Texto("Olá".to_string()));
    processar(Mensagem::Sair);
}

Exemplo 5: while let com padrões complexos

let mut pilha = vec![(1, "a"), (2, "b"), (3, "c")];

while let Some((num, letra)) = pilha.pop() {
    println!("{}: {}", num, letra);
}

Boas práticas

Use if let e while let para simplificar o código quando você só se importa com um padrão. Evite aninhar muitos if let; prefira match para múltiplos padrões. Lembre-se de que if let pode ser combinado com else e else if let, mas mantenha a legibilidade.

Referências

Exercícios

  1. Escreva uma função que recebe um Option<i32> e imprime o valor se for Some, caso contrário imprime "Nada". Use if let.

    ✓ Resposta:
    fn imprimir_opcao(valor: Option<i32>) {
        if let Some(x) = valor {
            println!("{}", x);
        } else {
            println!("Nada");
        }
    }
  2. Crie um loop que itere sobre um vetor de Result<u32, String> e imprima apenas os valores de sucesso, usando while let.

    ✓ Resposta:
    let resultados = vec![Ok(10), Err("erro".to_string()), Ok(20)];
    let mut iter = resultados.into_iter();
    
    while let Some(Ok(valor)) = iter.next() {
        println!("Sucesso: {}", valor);
    }
  3. Dado um enum Animal { Cachorro(String), Gato(u8) }, escreva uma função que recebe um Animal e imprime o nome se for Cachorro, ou a idade se for Gato. Use if let com else.

    ✓ Resposta:
    enum Animal {
        Cachorro(String),
        Gato(u8),
    }
    
    fn descrever(animal: Animal) {
        if let Animal::Cachorro(nome) = animal {
            println!("Cachorro: {}", nome);
        } else if let Animal::Gato(idade) = animal {
            println!("Gato: {} anos", idade);
        }
    }
  4. Use while let para consumir um iterador de char e imprimir cada caractere até encontrar um espaço.

    ✓ Resposta:
    let frase = "Olá mundo".chars();
    let mut iter = frase;
    
    while let Some(c) = iter.next() {
        if c == ' ' {
            break;
        }
        println!("{}", c);
    }
  5. Escreva um programa que lê linhas de um arquivo (simule com um vetor de Option<String>) e imprime cada linha, parando quando encontrar None. Use while let.

    ✓ Resposta:
    let linhas = vec![Some("linha1".to_string()), Some("linha2".to_string()), None, Some("linha3".to_string())];
    let mut iter = linhas.into_iter();
    
    while let Some(Some(linha)) = iter.next() {
        println!("{}", linha);
    }