O pattern matching é uma das características mais poderosas do Rust, permitindo que você compare valores contra padrões e execute código de forma concisa e segura. Nesta aula, vamos aprofundar nosso conhecimento, explorando padrões avançados que vão além dos casos simples de match e if let. Você aprenderá a usar ranges, desestruturação aninhada, @ bindings e padrões em funções, ferramentas essenciais para escrever código idiomático e expressivo.

Dominar esses recursos não só torna seu código mais limpo, mas também ajuda a evitar bugs, pois o compilador verifica a exaustividade e a corretude dos padrões. Vamos começar com padrões em ranges, que permitem combinar faixas de valores de forma elegante.

Padrões em ranges

Em Rust, você pode usar ranges em padrões para combinar um valor que esteja dentro de um intervalo. Isso é especialmente útil quando você deseja classificar valores numéricos (como notas, idades) ou caracteres (como letras). A sintaxe usa ..= para um range inclusivo e .. para um range exclusivo (embora em padrões, o exclusivo seja menos comum).

Por exemplo, suponha que você queira categorizar uma idade em faixas: criança (0-12), adolescente (13-19), adulto (20-59) e idoso (60+). Com ranges, o código fica muito mais legível do que usar condições if.

fn classificar_idade(idade: u8) -> &'static str {
    match idade {
        0..=12 => "Criança",
        13..=19 => "Adolescente",
        20..=59 => "Adulto",
        60..=u8::MAX => "Idoso",
    }
}

fn main() {
    println!("{}", classificar_idade(10)); // Criança
    println!("{}", classificar_idade(25)); // Adulto
    println!("{}", classificar_idade(70)); // Idoso
}

Ranges também funcionam com caracteres, permitindo combinar letras minúsculas, maiúsculas ou dígitos. Isso é útil em parsers ou validações.

fn categoria_char(c: char) -> &'static str {
    match c {
        'a'..='z' => "Letra minúscula",
        'A'..='Z' => "Letra maiúscula",
        '0'..='9' => "Dígito",
        _ => "Outro",
    }
}

É importante notar que ranges em padrões devem ser literais ou constantes, não variáveis, pois o compilador precisa conhecer os limites em tempo de compilação. Se você precisar de limites dinâmicos, use guardas (if) no padrão.

Desestruturação aninhada

Rust permite desestruturar estruturas de dados aninhadas (tuplas, structs, enums) em um único padrão, o que é extremamente poderoso para extrair valores de forma concisa. Isso é comum em combinação com match e if let.

Considere uma estrutura de dados que representa um ponto em um espaço 3D, mas com um campo opcional de cor. Podemos desestruturar tudo de uma vez.

#[derive(Debug)]
struct Ponto {
    x: i32,
    y: i32,
    z: i32,
    cor: Option<String>,
}

fn descrever(p: &Ponto) {
    match p {
        Ponto { x, y, z, cor: Some(cor) } => {
            println!("Ponto ({}, {}, {}) com cor {}", x, y, z, cor);
        }
        Ponto { x, y, z, cor: None } => {
            println!("Ponto ({}, {}, {}) sem cor", x, y, z);
        }
    }
}

fn main() {
    let p1 = Ponto { x: 1, y: 2, z: 3, cor: Some("vermelho".to_string()) };
    let p2 = Ponto { x: 4, y: 5, z: 6, cor: None };
    descrever(&p1);
    descrever(&p2);
}

Desestruturação aninhada também funciona com enums complexos. Por exemplo, uma árvore binária pode ser desestruturada recursivamente em um padrão.

enum Arvore<T> {
    Vazio,
    No(Box<Arvore<T>>, T, Box<Arvore<T>>),
}

fn soma_arvore(arvore: &Arvore<i32>) -> i32 {
    match arvore {
        Arvore::Vazio => 0,
        Arvore::No(esq, val, dir) => soma_arvore(esq) + val + soma_arvore(dir),
    }
}

Além disso, você pode usar .. para ignorar campos restantes em structs ou elementos em tuplas, simplificando ainda mais a desestruturação.

let tupla = (1, 2, 3, 4);
let (primeiro, .., ultimo) = tupla;
println!("primeiro: {}, ultimo: {}", primeiro, ultimo); // 1 e 4

@ bindings

O operador @ permite vincular um valor a uma variável enquanto também o testa contra um padrão. Isso é útil quando você quer usar o valor original dentro do braço do match, mas também verificar uma condição sobre ele (como um range ou uma variante).

Por exemplo, em um jogo, você pode querer verificar se um valor está dentro de um range e, se estiver, usar esse valor diretamente.

fn verificar_pontuacao(pontos: i32) -> String {
    match pontos {
        p @ 0..=50 => format!("Baixo ({} pontos)", p),
        p @ 51..=100 => format!("Médio ({} pontos)", p),
        p @ 101..=i32::MAX => format!("Alto ({} pontos)", p),
    }
}

No exemplo acima, p captura o valor original de pontos, permitindo que você o use na string formatada. Sem o @, você teria que usar pontos diretamente, o que não é possível dentro do padrão (pois ele é movido ou emprestado).

Outro uso comum é com enums, onde você quer verificar se um valor é uma variante específica e também extrair um campo.

enum Mensagem {
    Texto(String),
    Sair,
}

fn processar(msg: Mensagem) {
    match msg {
        m @ Mensagem::Texto(t) => println!("Mensagem: {} (tamanho: {})", t, t.len()),
        Mensagem::Sair => println!("Saindo..."),
    }
}

Note que m não é usado no exemplo, mas você poderia usá-lo se precisasse da mensagem completa. O @ é especialmente útil em padrões complexos, pois evita a repetição do valor.

Padrões em funções

Você pode usar padrões diretamente nos parâmetros de uma função, permitindo desestruturar argumentos de forma concisa. Isso é comum em funções que recebem tuplas, structs ou enums.

Por exemplo, uma função que recebe uma tupla de coordenadas pode desestruturá-la na assinatura.

fn distancia_origem((x, y): (f64, f64)) -> f64 {
    (x.powi(2) + y.powi(2)).sqrt()
}

fn main() {
    let ponto = (3.0, 4.0);
    println!("Distância: {}", distancia_origem(ponto)); // 5.0
}

Isso também funciona com structs, permitindo extrair campos diretamente.

struct Retangulo {
    largura: u32,
    altura: u32,
}

fn area(Retangulo { largura, altura }: &Retangulo) -> u32 {
    largura * altura
}

fn main() {
    let r = Retangulo { largura: 10, altura: 5 };
    println!("Área: {}", area(&r));
}

Além disso, padrões em funções podem incluir ranges e @ bindings, embora seja menos comum. Por exemplo, você pode ter uma função que só aceita números dentro de um range específico, mas isso exigiria uma guarda, que não é permitida na assinatura. No entanto, você pode usar padrões irrefutáveis (como desestruturação) que sempre funcionam.

É importante lembrar que padrões em parâmetros devem ser irrefutáveis, ou seja, devem sempre corresponder. Padrões refutáveis (como ranges) não são permitidos diretamente em parâmetros de função, pois o compilador não pode garantir que a chamada não falhe. Para isso, use match ou if let dentro do corpo.

Boas práticas e observações finais

Quando usar pattern matching avançado, procure manter a legibilidade. Padrões muito complexos podem dificultar a leitura do código. Use desestruturação aninhada com moderação e prefira extrair para variáveis quando o padrão ficar confuso.

Lembre-se de que o compilador verifica a exaustividade dos match, então sempre inclua um braço _ (coringa) quando necessário, a menos que todos os casos sejam cobertos. Isso evita erros em tempo de execução.

Outra dica: use @ bindings para evitar repetição de expressões, mas não abuse, pois pode tornar o código menos claro. Prefira nomes descritivos para as variáveis vinculadas.

Por fim, pratique! Escreva pequenos programas que usem esses padrões em situações reais, como processamento de dados, parsers ou jogos. Quanto mais você usar, mais natural se tornará.

Referências

Exercícios

  1. Escreva uma função que receba um número inteiro e retorne uma classificação: "negativo", "zero" ou "positivo". Use ranges em padrões.
  2. ✓ Resposta:
    fn classificar_numero(n: i32) -> &'static str {
        match n {
            i32::MIN..=-1 => "negativo",
            0 => "zero",
            1..=i32::MAX => "positivo",
        }
    }
  3. Dada a enum Forma com variantes Circulo { raio: f64 } e Retangulo { largura: f64, altura: f64 }, escreva uma função que retorne a área da forma usando desestruturação aninhada.
  4. ✓ Resposta:
    enum Forma {
        Circulo { raio: f64 },
        Retangulo { largura: f64, altura: f64 },
    }
    
    fn area(forma: Forma) -> f64 {
        match forma {
            Forma::Circulo { raio } => std::f64::consts::PI * raio.powi(2),
            Forma::Retangulo { largura, altura } => largura * altura,
        }
    }
  5. Explique a diferença entre .. e ..= em padrões de range e dê um exemplo de cada.
  6. ✓ Resposta: .. é um range exclusivo (não inclui o limite superior), enquanto ..= é inclusivo. Em padrões, .. raramente é usado porque não é possível representar um range exclusivo de forma útil (por exemplo, 1..5 em um padrão significaria 1,2,3,4, mas Rust não permite ranges exclusivos em padrões; apenas ..= é permitido. Na verdade, a sintaxe .. em padrões é usada para ignorar campos, não para ranges. Exemplo de range inclusivo: 1..=5; exemplo de uso de .. para ignorar: let (a, ..) = (1, 2, 3);.
  7. Escreva uma função que receba uma tupla (i32, i32) e retorne o maior valor, usando @ bindings para evitar duplicação.
  8. ✓ Resposta:
    fn maior((a, b): (i32, i32)) -> i32 {
        match (a, b) {
            (a @ 0..=i32::MAX, b @ 0..=i32::MAX) if a >= b => a,
            (a @ 0..=i32::MAX, b @ 0..=i32::MAX) => b,
            (a, b) => if a >= b { a } else { b },
        }
    }
  9. Usando padrões em funções, crie uma função que receba um Result<u32, String> e imprima o valor se for Ok, ou a mensagem de erro se for Err. Use desestruturação na assinatura.
  10. ✓ Resposta:
    fn processar_resultado(resultado: Result<u32, String>) {
        match resultado {
            Ok(valor) => println!("Valor: {}", valor),
            Err(erro) => println!("Erro: {}", erro),
        }
    }
    
    // Alternativa com if let:
    fn processar_resultado(resultado: Result<u32, String>) {
        if let Ok(valor) = resultado {
            println!("Valor: {}", valor);
        } else if let Err(erro) = resultado {
            println!("Erro: {}", erro);
        }
    }