O pattern matching é uma das características mais poderosas de Rust, permitindo que você compare um valor contra uma série de padrões e execute código diferente para cada combinação. O match é a principal ferramenta para isso, e ele é exaustivo: você deve cobrir todas as possibilidades, garantindo segurança em tempo de compilação. Nesta aula, exploraremos a fundo o match, incluindo padrões, guardas, o curinga _ e o binding de valores.

Dominar o match é essencial para escrever código idiomático em Rust, especialmente ao trabalhar com enums, opcionais e tipos complexos. Vamos começar com o conceito de exaustividade e depois mergulhar em tópicos avançados.

match exaustivo

Em Rust, o match é exaustivo: você deve cobrir todos os casos possíveis do valor que está sendo analisado. Se você esquecer um caso, o compilador emitirá um erro. Isso elimina bugs comuns de linguagens que não têm essa exigência, como esquecer de tratar um valor nulo ou uma variante de enum.

Por exemplo, considere um enum simples:

enum Cor {
    Vermelho,
    Verde,
    Azul,
}

fn descrever(cor: Cor) -> &'static str {
    match cor {
        Cor::Vermelho => "vermelho",
        Cor::Verde => "verde",
        Cor::Azul => "azul",
    }
}

Se você remover um dos braços, o compilador rejeitará o código. A exaustividade se aplica a qualquer tipo, incluindo inteiros, chars e booleanos. Para tipos primitivos, você pode usar ranges e listas para cobrir múltiplos valores.

Exemplo com números:

fn classificar_numero(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1..=10 => "pequeno",
        11..=100 => "médio",
        _ => "grande",
    }
}

O padrão _ (catch-all) é útil para cobrir todos os casos restantes. Sem ele, o compilador exigiria que você listasse todos os valores possíveis de i32, o que é inviável.

Padrões e guardas

Além de padrões simples, o match suporta guardas, que são condições adicionais que devem ser verdadeiras para que o braço seja executado. Guardas são escritas com a palavra-chave if após o padrão.

Exemplo com guarda:

fn descrever_numero(n: i32) -> &'static str {
    match n {
        0 => "zero",
        n if n < 0 => "negativo",
        n if n > 0 && n <= 10 => "pequeno positivo",
        _ => "outro",
    }
}

Guardas permitem expressões booleanas arbitrárias, incluindo chamadas de função. Elas são avaliadas apenas se o padrão corresponder primeiro.

Padrões também podem ser combinados com | para representar múltiplos valores:

fn vogal(c: char) -> bool {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => true,
        _ => false,
    }
}

Você pode usar padrões em outros lugares, como em if let e while let, que são açúcares sintáticos para casos específicos de match.

_ catch-all

O padrão _ é um curinga que corresponde a qualquer valor. Ele é frequentemente usado no último braço do match para cobrir todos os casos não especificados. Além disso, você pode usar _ dentro de padrões para ignorar partes de um valor, como em tuplas ou structs.

Exemplo ignorando um campo de uma tupla:

let tupla = (1, 2, 3);
match tupla {
    (x, _, z) => println!("x={}, z={}", x, z),
}

O _ não faz binding: você não pode usar o valor ignorado. Se você quiser usar o valor, mas não se importa com o nome, pode usar _nome (com underline prefixado), que ainda faz binding, mas suprime warnings de variável não usada.

Outro uso comum é em match com enums que têm dados:

enum Mensagem {
    Sair,
    Mover { x: i32, y: i32 },
    Escrever(String),
}

fn processar(msg: Mensagem) {
    match msg {
        Mensagem::Sair => println!("saindo"),
        Mensagem::Mover { x, y } => println!("move para ({}, {})", x, y),
        Mensagem::Escrever(_) => println!("escreveu algo"),
    }
}

No braço Escrever(_), ignoramos o conteúdo da string. Isso é útil quando queremos tratar a variante sem extrair os dados.

Binding

O binding em match permite que você capture partes do valor correspondente e as vincule a variáveis. Isso é feito automaticamente quando você nomeia os campos ou usa @ para vincular um valor a uma variável enquanto também testa o padrão.

Exemplo básico de binding:

enum Opcao {
    Algum(i32),
    Nenhum,
}

fn obter_valor(op: Opcao) -> i32 {
    match op {
        Opcao::Algum(x) => x,
        Opcao::Nenhum => 0,
    }
}

Aqui, x é vinculado ao valor dentro de Algum. Você também pode usar @ para vincular um valor que também é testado por um padrão:

fn descrever_numero(n: i32) -> &'static str {
    match n {
        x @ 0..=10 => {
            println!("x é {}", x);
            "pequeno"
        },
        _ => "grande",
    }
}

O padrão x @ 0..=10 testa se n está no intervalo e, se sim, vincula o valor a x. Isso é útil quando você precisa do valor original dentro do braço.

Binding também funciona com structs e enums com campos nomeados:

struct Ponto {
    x: i32,
    y: i32,
}

fn origem(p: Ponto) -> bool {
    match p {
        Ponto { x: 0, y: 0 } => true,
        Ponto { x, y } => false, // x e y são vinculados
    }
}

Nesse caso, x e y são vinculados aos valores respectivos. Você pode usar .. para ignorar campos restantes.

O binding combinado com if let é uma forma concisa de extrair valores:

if let Some(valor) = opcao {
    println!("valor: {}", valor);
}

Isso é equivalente a um match de um braço.

Boas práticas

Prefira match a cadeias de if-else quando você está comparando um valor contra várias alternativas, pois o match é mais legível e o compilador verifica exaustividade. Use guardas para condições complexas que não podem ser expressas apenas com padrões. Evite usar _ indiscriminadamente; sempre que possível, nomeie os casos para maior clareza. Lembre-se de que _ não faz binding, então use _nome se precisar referir-se ao valor.

Referências

Exercícios

  1. Escreva uma função que recebe um Option<i32> e retorna o valor interno se for Some, ou 0 se for None. Use match.
  2. ✓ Resposta:
    fn extrair(op: Option<i32>) -> i32 {
        match op {
            Some(val) => val,
            None => 0,
        }
    }
  3. Dado um enum Resultado com variantes Sucesso(i32) e Erro(String), escreva uma função que retorna uma string descrevendo o resultado. Para Sucesso, retorne "Sucesso: {valor}"; para Erro, retorne "Erro: {mensagem}".
  4. ✓ Resposta:
    enum Resultado {
        Sucesso(i32),
        Erro(String),
    }
    
    fn descrever(res: Resultado) -> String {
        match res {
            Resultado::Sucesso(val) => format!("Sucesso: {}", val),
            Resultado::Erro(msg) => format!("Erro: {}", msg),
        }
    }
  5. Use um guarda em um match para classificar um número como "positivo", "negativo" ou "zero".
  6. ✓ Resposta:
    fn classificar(n: i32) -> &'static str {
        match n {
            n if n > 0 => "positivo",
            n if n < 0 => "negativo",
            _ => "zero",
        }
    }
  7. Dada uma tupla (i32, i32, i32), use match com _ para ignorar o segundo elemento e imprimir apenas o primeiro e o terceiro.
  8. ✓ Resposta:
    fn imprimir_primeiro_terceiro(t: (i32, i32, i32)) {
        match t {
            (a, _, c) => println!("primeiro: {}, terceiro: {}", a, c),
        }
    }
  9. Escreva uma função que recebe um Result<i32, String> e, usando match com binding, retorna o dobro do valor se for Ok, ou a string de erro concatenada com "!" se for Err.
  10. ✓ Resposta:
    fn processar(res: Result<i32, String>) -> String {
        match res {
            Ok(val) => (val * 2).to_string(),
            Err(msg) => format!("{}!", msg),
        }
    }