Pattern Matching Avançado em Rust: Guia | Rust Brasil

Pattern matching avançado em Rust: match guards, bindings, nested patterns, if let, while let e padrões com structs.

Pattern matching é um dos recursos mais expressivos do Rust. Enquanto um simples switch de outras linguagens compara apenas valores, o match do Rust permite decompor estruturas de dados complexas, vincular variáveis, aplicar condições e garantir que todos os casos sejam cobertos. Neste artigo, vamos além do básico e explorar padrões avançados que tornam o código Rust elegante e robusto.

Revisão Rápida: O Básico de match

fn classificar_numero(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1..=9 => "um dígito",
        10..=99 => "dois dígitos",
        100..=999 => "três dígitos",
        _ => "muitos dígitos",
    }
}

fn main() {
    println!("{}", classificar_numero(42));  // dois dígitos
    println!("{}", classificar_numero(0));   // zero
    println!("{}", classificar_numero(1000)); // muitos dígitos
}

O match em Rust é exaustivo — você deve cobrir todos os casos possíveis, ou o código não compila. Isso elimina bugs silenciosos por esquecimento.

Destructuring em Profundidade

Destructuring de Structs

struct Ponto {
    x: f64,
    y: f64,
    z: f64,
}

fn classificar_ponto(p: &Ponto) -> &'static str {
    match p {
        Ponto { x: 0.0, y: 0.0, z: 0.0 } => "origem",
        Ponto { z: 0.0, .. } => "no plano XY",
        Ponto { x, y, .. } if (x * x + y * y).sqrt() < 1.0 => "perto do eixo Z",
        _ => "no espaço 3D",
    }
}

fn main() {
    let pontos = vec![
        Ponto { x: 0.0, y: 0.0, z: 0.0 },
        Ponto { x: 3.0, y: 4.0, z: 0.0 },
        Ponto { x: 0.1, y: 0.2, z: 5.0 },
        Ponto { x: 10.0, y: 20.0, z: 30.0 },
    ];

    for p in &pontos {
        println!("({}, {}, {}) → {}", p.x, p.y, p.z, classificar_ponto(p));
    }
}

O .. ignora campos que não nos interessam. Podemos também renomear campos na captura:

struct Config {
    modo: String,
    porta: u16,
    debug: bool,
}

fn descrever(config: &Config) {
    match config {
        Config { debug: true, porta, .. } => {
            println!("Modo debug na porta {}", porta);
        }
        Config { modo, porta, .. } => {
            println!("Modo '{}' na porta {}", modo, porta);
        }
    }
}

fn main() {
    let config = Config {
        modo: "produção".into(),
        porta: 8080,
        debug: false,
    };
    descrever(&config);
}

Destructuring de Enums Aninhados

#[derive(Debug)]
enum Forma {
    Circulo { raio: f64 },
    Retangulo { largura: f64, altura: f64 },
    Triangulo { base: f64, altura: f64 },
}

#[derive(Debug)]
enum Comando {
    Criar(Forma),
    Mover { dx: f64, dy: f64 },
    Escalar(f64),
    Deletar,
}

fn processar(cmd: &Comando) {
    match cmd {
        Comando::Criar(Forma::Circulo { raio }) => {
            println!("Criando círculo com raio {:.1}", raio);
        }
        Comando::Criar(Forma::Retangulo { largura, altura }) => {
            println!("Criando retângulo {}x{}", largura, altura);
        }
        Comando::Criar(forma) => {
            println!("Criando forma: {:?}", forma);
        }
        Comando::Mover { dx, dy } => {
            println!("Movendo ({}, {})", dx, dy);
        }
        Comando::Escalar(fator) if *fator > 1.0 => {
            println!("Ampliando por {:.1}x", fator);
        }
        Comando::Escalar(fator) => {
            println!("Reduzindo por {:.1}x", fator);
        }
        Comando::Deletar => println!("Deletando"),
    }
}

fn main() {
    let comandos = vec![
        Comando::Criar(Forma::Circulo { raio: 5.0 }),
        Comando::Criar(Forma::Retangulo { largura: 10.0, altura: 20.0 }),
        Comando::Mover { dx: 1.0, dy: -2.0 },
        Comando::Escalar(2.5),
        Comando::Escalar(0.5),
        Comando::Deletar,
    ];

    for cmd in &comandos {
        processar(cmd);
    }
}

Destructuring de Tuplas e Slices

fn analisar_dados(dados: &[i32]) {
    match dados {
        [] => println!("Vazio"),
        [unico] => println!("Um elemento: {}", unico),
        [primeiro, segundo] => println!("Par: ({}, {})", primeiro, segundo),
        [primeiro, .., ultimo] => {
            println!("Sequência: {} ... {} ({} elementos)", primeiro, ultimo, dados.len());
        }
    }
}

fn classificar_coordenada(coord: (i32, i32)) {
    match coord {
        (0, 0) => println!("Origem"),
        (x, 0) => println!("Eixo X em {}", x),
        (0, y) => println!("Eixo Y em {}", y),
        (x, y) if x == y => println!("Diagonal principal em ({}, {})", x, y),
        (x, y) if x == -y => println!("Diagonal secundária em ({}, {})", x, y),
        (x, y) => println!("Ponto ({}, {})", x, y),
    }
}

fn main() {
    analisar_dados(&[]);
    analisar_dados(&[42]);
    analisar_dados(&[1, 2]);
    analisar_dados(&[1, 2, 3, 4, 5]);

    classificar_coordenada((0, 0));
    classificar_coordenada((5, 0));
    classificar_coordenada((3, 3));
    classificar_coordenada((2, -2));
    classificar_coordenada((1, 7));
}

Match Guards

Guards são condições adicionais com if depois do padrão:

#[derive(Debug)]
struct Pedido {
    valor: f64,
    itens: u32,
    frete_gratis: bool,
}

fn calcular_frete(pedido: &Pedido) -> f64 {
    match pedido {
        Pedido { frete_gratis: true, .. } => 0.0,
        Pedido { valor, .. } if *valor > 200.0 => 0.0, // frete grátis acima de R$200
        Pedido { itens, .. } if *itens == 1 => 9.90,
        Pedido { itens, .. } if *itens <= 5 => 14.90,
        _ => 24.90,
    }
}

fn main() {
    let pedidos = vec![
        Pedido { valor: 50.0, itens: 1, frete_gratis: false },
        Pedido { valor: 250.0, itens: 3, frete_gratis: false },
        Pedido { valor: 30.0, itens: 3, frete_gratis: true },
        Pedido { valor: 100.0, itens: 8, frete_gratis: false },
    ];

    for p in &pedidos {
        println!("Pedido R${:.2} ({} itens) → frete R${:.2}",
            p.valor, p.itens, calcular_frete(p));
    }
}

Cuidado: Guards Não São Capturados na Exhaustiveness

fn exemplo(valor: Option<i32>) {
    match valor {
        Some(x) if x > 0 => println!("positivo: {}", x),
        Some(x) if x < 0 => println!("negativo: {}", x),
        // O compilador NÃO sabe que os guards cobrem todos os casos
        // Você precisa de um braço para Some(0) ou um wildcard
        Some(x) => println!("zero: {}", x),
        None => println!("nenhum"),
    }
}

fn main() {
    exemplo(Some(5));
    exemplo(Some(-3));
    exemplo(Some(0));
    exemplo(None);
}

Or-Patterns (|)

Use | para combinar múltiplos padrões em um único braço:

fn dia_util(dia: &str) -> bool {
    match dia {
        "segunda" | "terça" | "quarta" | "quinta" | "sexta" => true,
        "sábado" | "domingo" => false,
        _ => panic!("Dia inválido: {}", dia),
    }
}

fn classificar_char(c: char) -> &'static str {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u'
        | 'A' | 'E' | 'I' | 'O' | 'U' => "vogal",
        '0'..='9' => "dígito",
        ' ' | '\t' | '\n' | '\r' => "espaço em branco",
        _ => "outro",
    }
}

fn main() {
    println!("segunda é dia útil? {}", dia_util("segunda"));
    println!("sábado é dia útil? {}", dia_util("sábado"));

    for c in "Rust 2026!".chars() {
        println!("'{}' → {}", c, classificar_char(c));
    }
}

Or-Patterns com Binding (desde Rust 1.56)

enum Resultado {
    Sucesso(String),
    Aviso(String),
    Erro(String),
}

fn mensagem(resultado: Resultado) -> String {
    match resultado {
        Resultado::Sucesso(msg) | Resultado::Aviso(msg) | Resultado::Erro(msg) => msg,
    }
}

fn tipo_resultado(resultado: &Resultado) -> &str {
    match resultado {
        Resultado::Sucesso(_) => "sucesso",
        Resultado::Aviso(_) => "aviso",
        Resultado::Erro(_) => "erro",
    }
}

fn main() {
    let r = Resultado::Aviso("Atenção: disco quase cheio".into());
    println!("[{}] {}", tipo_resultado(&r), mensagem(r));
}

Binding com @

O operador @ permite vincular um nome a um valor enquanto testa um padrão:

fn classificar_idade(idade: u32) -> String {
    match idade {
        id @ 0..=12 => format!("Criança ({} anos)", id),
        id @ 13..=17 => format!("Adolescente ({} anos)", id),
        id @ 18..=64 => format!("Adulto ({} anos)", id),
        id @ 65.. => format!("Idoso ({} anos)", id),
    }
}

#[derive(Debug)]
enum Mensagem {
    Texto(String),
    Imagem { url: String, tamanho_kb: u32 },
}

fn processar_mensagem(msg: &Mensagem) {
    match msg {
        Mensagem::Imagem { tamanho_kb: tam @ 0..=100, url } => {
            println!("Imagem pequena ({}KB): {}", tam, url);
        }
        Mensagem::Imagem { tamanho_kb: tam @ 101..=1000, url } => {
            println!("Imagem média ({}KB): {}", tam, url);
        }
        img @ Mensagem::Imagem { .. } => {
            println!("Imagem grande: {:?}", img);
        }
        Mensagem::Texto(t) if t.len() > 100 => {
            println!("Texto longo ({} chars)", t.len());
        }
        Mensagem::Texto(t) => {
            println!("Texto: {}", t);
        }
    }
}

fn main() {
    println!("{}", classificar_idade(8));    // Criança (8 anos)
    println!("{}", classificar_idade(16));   // Adolescente (16 anos)
    println!("{}", classificar_idade(30));   // Adulto (30 anos)
    println!("{}", classificar_idade(70));   // Idoso (70 anos)

    processar_mensagem(&Mensagem::Texto("Olá!".into()));
    processar_mensagem(&Mensagem::Imagem {
        url: "foto.jpg".into(),
        tamanho_kb: 50,
    });
    processar_mensagem(&Mensagem::Imagem {
        url: "panorama.png".into(),
        tamanho_kb: 5000,
    });
}

Match Ergonomics (Ergonomia de Match)

Desde Rust 1.26, o match faz binding automático por referência quando o valor sendo comparado é uma referência:

fn main() {
    let dados: &Option<String> = &Some("hello".to_string());

    // Antes de match ergonomics — verboso:
    match dados {
        &Some(ref s) => println!("Valor: {}", s),
        &None => println!("Nenhum"),
    }

    // Com match ergonomics — limpo:
    match dados {
        Some(s) => println!("Valor: {}", s),   // s é &String automaticamente
        None => println!("Nenhum"),
    }

    // Funciona com if let também
    if let Some(s) = dados {
        println!("Encontrado: {}", s); // s é &String
    }
}

Isso funciona com structs também:

#[derive(Debug)]
struct Usuario {
    nome: String,
    email: String,
    ativo: bool,
}

fn listar_ativos(usuarios: &[Usuario]) {
    for usuario in usuarios {
        // Match ergonomics: usuario já é &Usuario
        // nome e email são automaticamente &String
        match usuario {
            Usuario { nome, ativo: true, .. } => {
                println!("Ativo: {}", nome);
            }
            Usuario { nome, ativo: false, .. } => {
                println!("Inativo: {}", nome);
            }
        }
    }
}

fn main() {
    let usuarios = vec![
        Usuario { nome: "Ana".into(), email: "ana@ex.com".into(), ativo: true },
        Usuario { nome: "Bruno".into(), email: "bruno@ex.com".into(), ativo: false },
        Usuario { nome: "Clara".into(), email: "clara@ex.com".into(), ativo: true },
    ];
    listar_ativos(&usuarios);
}

Exhaustiveness: O Poder da Cobertura Completa

Uma das maiores vantagens do match em Rust é a verificação de exaustividade em tempo de compilação:

enum Permissao {
    Leitura,
    Escrita,
    Admin,
}

fn nivel_acesso(perm: &Permissao) -> u8 {
    match perm {
        Permissao::Leitura => 1,
        Permissao::Escrita => 2,
        Permissao::Admin => 3,
        // Se adicionarmos uma nova variante ao enum,
        // o compilador nos avisará que falta um braço aqui!
    }
}

fn main() {
    println!("Admin: nível {}", nivel_acesso(&Permissao::Admin));
}

Non-exhaustive enums de bibliotecas externas

Algumas bibliotecas marcam enums com #[non_exhaustive], obrigando o uso de _:

#[non_exhaustive]
enum StatusHttp {
    Ok,
    NotFound,
    InternalError,
}

fn tratar_status(status: StatusHttp) -> &'static str {
    match status {
        StatusHttp::Ok => "Sucesso",
        StatusHttp::NotFound => "Não encontrado",
        StatusHttp::InternalError => "Erro interno",
        _ => "Status desconhecido", // Obrigatório com #[non_exhaustive]
    }
}

fn main() {
    println!("{}", tratar_status(StatusHttp::Ok));
}

if let e while let

Para casos simples, if let e while let são mais concisos:

fn main() {
    let config: Option<(String, u16)> = Some(("localhost".into(), 8080));

    // if let com destructuring
    if let Some((host, porta)) = &config {
        println!("Conectando em {}:{}", host, porta);
    }

    // while let com iterador
    let mut pilha = vec![1, 2, 3, 4, 5];
    while let Some(topo) = pilha.pop() {
        println!("Processando: {}", topo);
    }

    // let-else (Rust 1.65+)
    let texto = "42";
    let Ok(numero) = texto.parse::<i32>() else {
        println!("Falha ao converter");
        return;
    };
    println!("Número: {}", numero);
}

Erros Comuns

1. Esquecer um braço do match

O compilador vai reclamar se você não cobrir todos os casos. Use _ como último braço se os demais não importam.

2. Tentar mutar dentro de um match por referência

fn main() {
    let mut valores = vec![Some(1), None, Some(3)];

    for valor in &mut valores {
        match valor {
            Some(ref mut v) => *v *= 2, // use ref mut explicitamente
            None => {}
        }
    }

    println!("{:?}", valores); // [Some(2), None, Some(6)]
}

Aplicações no Mundo Real: Parser de Expressões

#[derive(Debug, Clone)]
enum Expr {
    Numero(f64),
    Soma(Box<Expr>, Box<Expr>),
    Mult(Box<Expr>, Box<Expr>),
    Neg(Box<Expr>),
}

fn avaliar(expr: &Expr) -> f64 {
    match expr {
        Expr::Numero(n) => *n,
        Expr::Soma(a, b) => avaliar(a) + avaliar(b),
        Expr::Mult(a, b) => avaliar(a) * avaliar(b),
        Expr::Neg(e) => -avaliar(e),
    }
}

fn formatar(expr: &Expr) -> String {
    match expr {
        Expr::Numero(n) => format!("{}", n),
        Expr::Soma(a, b) => format!("({} + {})", formatar(a), formatar(b)),
        Expr::Mult(a, b) => format!("({} * {})", formatar(a), formatar(b)),
        Expr::Neg(e) => format!("(-{})", formatar(e)),
    }
}

fn main() {
    // (2 + 3) * -(4)
    let expr = Expr::Mult(
        Box::new(Expr::Soma(
            Box::new(Expr::Numero(2.0)),
            Box::new(Expr::Numero(3.0)),
        )),
        Box::new(Expr::Neg(Box::new(Expr::Numero(4.0)))),
    );

    println!("{} = {}", formatar(&expr), avaliar(&expr));
    // ((2 + 3) * (-4)) = -20
}

Veja Também