O pattern matching é um dos recursos mais poderosos de Rust, permitindo que você compare um valor contra uma série de padrões e execute código com base no padrão que corresponde. A expressão match é a principal ferramenta para isso, oferecendo segurança e expressividade. Nesta aula, vamos nos aprofundar nos conceitos de exaustividade, guardas, padrões curinga e binding, que são fundamentais para dominar o uso de match.

Diferente de if ou switch de outras linguagens, o match em Rust é exaustivo: o compilador verifica se todos os casos possíveis foram cobertos, evitando erros em tempo de execução. Além disso, você pode adicionar condições extras com guardas, capturar valores com binding e usar o padrão _ para ignorar casos não desejados. Vamos ver cada um desses tópicos em detalhes.

match exaustivo

Em Rust, a expressão match exige que todos os valores possíveis de um tipo sejam tratados. Isso é chamado de exaustividade. Se você omitir um braço, o compilador emitirá um erro. Isso garante que você não esqueça de lidar com casos importantes, especialmente ao trabalhar com enums ou tipos que podem ter múltiplas variantes.

Por exemplo, considere uma enumeração simples:

enum Cor {
    Vermelho,
    Verde,
    Azul,
}

fn descrever_cor(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 é especialmente útil com enums como Option<T> e Result<T, E>, forçando você a tratar tanto Some quanto None, ou Ok e Err.

Padrões e guardas

Além de simplesmente corresponder a um valor, você pode adicionar condições extras chamadas guardas (usando a palavra-chave if após o padrão). Guardas permitem refinar a correspondência com uma expressão booleana. Elas são úteis quando você precisa de lógica condicional dentro de um braço.

Exemplo com números:

fn classificar_numero(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1..=10 if n % 2 == 0 => "par entre 1 e 10",
        1..=10 => "ímpar entre 1 e 10",
        _ => "fora do intervalo",
    }
}

Note que a guarda é avaliada apenas se o padrão corresponder. Se a guarda for falsa, o match continua testando os próximos braços. Isso permite combinar padrões com condições dinâmicas.

_ catch-all

O padrão _ é um curinga que corresponde a qualquer valor, mas sem fazer binding (ou seja, não captura o valor). Ele é usado como um braço catch-all para cobrir todos os casos não tratados explicitamente. Como o match é exaustivo, você precisa de um braço catch-all (ou usar um padrão que cubra todos os casos restantes).

Exemplo:

fn o_que_eh(x: i32) -> &'static str {
    match x {
        1 => "um",
        2 => "dois",
        _ => "outro número",
    }
}

O _ é útil quando você não se importa com o valor específico, apenas quer garantir que todos os casos estão cobertos. Ele também pode ser usado em outros contextos de pattern matching, como em if let e while let.

Binding

Binding refere-se à capacidade de capturar o valor correspondido e vinculá-lo a uma variável. No match, você pode usar o padrão com um nome de variável para capturar o valor. Além disso, Rust oferece o operador @ para fazer binding em subpadrões.

Exemplo simples de binding:

fn descrever_opcao(opcao: Option<i32>) -> String {
    match opcao {
        Some(x) => format!("Valor: {}", x),
        None => String::from("Nada"),
    }
}

O x captura o valor dentro de Some. Você também pode usar @ para capturar um valor enquanto também testa um padrão mais específico:

fn testar_numero(n: i32) -> &'static str {
    match n {
        x @ 1..=10 => "número pequeno",
        x @ 11..=100 => "número médio",
        _ => "número grande",
    }
}

Aqui, x captura o valor de n dentro do intervalo. O binding é útil quando você precisa usar o valor correspondido dentro do braço.

Boas práticas

Sempre prefira match a if-else quando estiver lidando com enums ou múltiplas condições. A exaustividade do compilador ajuda a evitar bugs. Use guardas com moderação para manter a legibilidade. O padrão _ é ótimo para casos catch-all, mas evite usá-lo quando você poderia nomear o valor para tornar o código mais expressivo. O binding com @ pode tornar o código mais conciso, mas não exagere.

Referências

Exercícios

  1. Crie uma enum chamada Mensagem com variantes Sair, Mover { x: i32, y: i32 } e Escrever(String). Escreva uma função que receba um Mensagem e use match para imprimir uma mensagem apropriada para cada variante.

    ✓ Resposta:
    enum Mensagem {
        Sair,
        Mover { x: i32, y: i32 },
        Escrever(String),
    }
    
    fn processar_mensagem(msg: Mensagem) {
        match msg {
            Mensagem::Sair => println!("Saindo..."),
            Mensagem::Mover { x, y } => println!("Movendo para ({}, {})", x, y),
            Mensagem::Escrever(texto) => println!("Mensagem: {}", texto),
        }
    }
  2. Escreva uma função que receba um Option<i32> e retorne o valor multiplicado por 2 se for Some, ou 0 se for None. Use match.

    ✓ Resposta:
    fn dobrar(opcao: Option) -> i32 {
        match opcao {
            Some(valor) => valor * 2,
            None => 0,
        }
    }
  3. Use guarda para escrever uma função que classifique um número inteiro como "positivo", "negativo" ou "zero" usando match.

    ✓ Resposta:
    fn classificar(n: i32) -> &'static str {
        match n {
            0 => "zero",
            x if x > 0 => "positivo",
            _ => "negativo",
        }
    }
  4. Explique por que o código abaixo não compila e corrija-o usando _ ou um padrão catch-all.

    fn exemplo(x: i32) -> &'static str {
        match x {
            1 => "um",
            2 => "dois",
        }
    }

    ✓ Resposta: O código não compila porque o match não é exaustivo: falta um braço para todos os outros valores de i32 (ex: 0, 3, -1, etc.). Corrigido:
    fn exemplo(x: i32) -> &'static str {
        match x {
            1 => "um",
            2 => "dois",
            _ => "outro",
        }
    }
  5. Usando binding com @, escreva uma função que receba um número e retorne uma string dizendo se ele está entre 1 e 10, entre 11 e 20, ou fora desses intervalos, capturando o valor.

    ✓ Resposta:
    fn faixa(n: i32) -> String {
        match n {
            x @ 1..=10 => format!("{} está entre 1 e 10", x),
            x @ 11..=20 => format!("{} está entre 11 e 20", x),
            x => format!("{} está fora dos intervalos", x),
        }
    }