Introdução ao Pattern Matching em Rust
Pattern matching é um dos recursos mais poderosos de Rust. Ele permite que você desconstrua valores complexos de forma segura e expressiva, comparando-os contra padrões específicos. Diferente de simples comparações, pattern matching em Rust verifica exaustividade — o compilador garante que você tratou todos os casos possíveis, eliminando erros lógicos comuns.
Rust oferece duas construções principais: match para análise completa e if let para casos simples. Ambas funcionam com enums, tuplas, structs e até literais. A verdadeira magia está em como o compilador força você a ser exhaustivo e seguro, rejeitando código incompleto em tempo de compilação.
A Expressão match: Poder Total
O match é uma expressão que avalia um padrão contra múltiplos braços. Ao contrário de switches em outras linguagens, match em Rust é exhaustivo — você deve cobrir todos os casos possíveis ou usar o padrão coringa _.
Exemplo Básico com Enum
enum Resultado {
Sucesso(String),
Erro(i32),
Pendente,
}
fn processar(res: Resultado) -> String {
match res {
Resultado::Sucesso(mensagem) => {
format!("✓ {}", mensagem)
}
Resultado::Erro(codigo) => {
format!("✗ Erro {}", codigo)
}
Resultado::Pendente => String::from("⏳ Aguardando..."),
}
}
fn main() {
let resultado = Resultado::Sucesso("Dados carregados".to_string());
println!("{}", processar(resultado)); // ✓ Dados carregados
}
Neste exemplo, cada variante do enum é tratada explicitamente. Se você esquecer um caso, o compilador recusa compilar o código. A desconstrução Sucesso(mensagem) extrai o valor interno automaticamente.
Padrões Avançados
O match suporta padrões complexos com guards (condições extras) e desestruturação profunda:
fn analisar_ponto(ponto: (i32, i32)) -> &'static str {
match ponto {
(0, 0) => "Origem",
(0, y) if y > 0 => "Eixo Y positivo",
(0, _) => "Eixo Y negativo",
(x, 0) if x > 0 => "Eixo X positivo",
(x, 0) => "Eixo X negativo",
(x, y) if x == y => "Diagonal principal",
(x, y) if x == -y => "Diagonal secundária",
_ => "Ponto genérico",
}
}
fn main() {
println!("{}", analisar_ponto((0, 5))); // Eixo Y positivo
println!("{}", analisar_ponto((3, 3))); // Diagonal principal
println!("{}", analisar_ponto((4, 2))); // Ponto genérico
}
Os guards com if permitem condições extras além do padrão estrutural. O padrão _ captura qualquer valor sem usá-lo — perfeito para casos que você não precisa processar.
if let: Simplicidade para Casos Únicos
Quando você só se importa com um padrão específico, if let oferece sintaxe mais clara e concisa. Ele é açúcar sintático sobre match com dois braços: um padrão que interessa e _ implícito para o resto.
Quando Usar if let
enum Cargo {
Presidente(String),
Desenvolvedor(String),
Indefinido,
}
fn verificar_desenvolvedor(cargo: Cargo) {
if let Cargo::Desenvolvedor(nome) = cargo {
println!("Desenvolvedor: {}", nome);
}
}
fn main() {
let meu_cargo = Cargo::Desenvolvedor("Alice".to_string());
verificar_desenvolvedor(meu_cargo); // Desenvolvedor: Alice
}
A vantagem aqui é legibilidade. Se você só precisa agir quando o cargo é desenvolvedor, if let é mais direto que match com vários braços vazios.
Combinando com else if let
Para múltiplas condições correlatas, encadeie if let:
fn classificar_valor(valor: Option<i32>) {
if let Some(n) = valor {
if n > 100 {
println!("Grande: {}", n);
} else {
println!("Pequeno: {}", n);
}
} else {
println!("Sem valor");
}
}
fn main() {
classificar_valor(Some(150)); // Grande: 150
classificar_valor(None); // Sem valor
}
Alternativamente, use else if let para sintaxe mais plana:
fn classificar_resultado(res: Result<i32, String>) {
if let Ok(num) = res {
println!("Sucesso: {}", num);
} else if let Err(erro) = res {
println!("Erro: {}", erro);
}
}
Padrões Avançados e Desestruturação
Rust permite desestruturação profunda em structs e enums aninhados, tornando code muito expressivo sem variáveis intermediárias.
Desestruturação de Structs
struct Usuario {
nome: String,
idade: u32,
email: Option<String>,
}
fn processar_usuario(usuario: Usuario) {
match usuario {
Usuario { nome, idade: 18..=65, email: Some(e) } => {
println!("{} ({}) - {}", nome, idade, e);
}
Usuario { nome, idade: 18..=65, email: None } => {
println!("{} ({}) - sem email", nome, idade);
}
Usuario { nome, idade, .. } => {
println!("{} - fora da faixa etária ({})", nome, idade);
}
}
}
fn main() {
let user = Usuario {
nome: "Bob".to_string(),
idade: 30,
email: Some("bob@example.com".to_string()),
};
processar_usuario(user);
}
Os ranges 18..=65 e o coringa .. para ignorar campos desnecessários tornam o padrão altamente legível. O compilador valida que todos os campos necessários são cobertos.
Ranges e Literais
fn descrever_dia(dia: u32) -> &'static str {
match dia {
1 | 3 | 5 | 7 | 9 | 11 => "Mês com 31 dias",
4 | 6 | 9 | 11 => "Mês com 30 dias",
2 => "Fevereiro",
_ => "Dia inválido",
}
}
fn categoria_idade(idade: u32) -> &'static str {
match idade {
0..=12 => "Criança",
13..=19 => "Adolescente",
20..=59 => "Adulto",
60.. => "Idoso",
_ => unreachable!(),
}
}
fn main() {
println!("{}", categoria_idade(25)); // Adulto
}
Padrões de múltiplas alternativas com | e ranges com .. cobrem casos comuns sem verbosidade. O unreachable!() marca código que nunca deve ser alcançado, ajudando o compilador com análise de fluxo.
Conclusão
Pattern matching é fundamental em Rust porque garante segurança em tempo de compilação. O compilador força você a considerar todos os cenários, eliminando bugs antes que ocorram. Use match para análise exaustiva e múltiplos padrões; use if let para casos únicos onde você só se importa com um resultado específico.
Desestruturação reduz código redundante — em vez de acessar campos manualmente, deixe o padrão extrair valores diretamente nas variáveis que você precisa. Guards e ranges tornam lógica complexa expressa elegantemente.
O compilador é seu aliado — rejeições iniciais parecem frustrantes, mas protegem você de horas de debugging. Abraçe a natureza exhaustiva do pattern matching e escreva código que é ao mesmo tempo seguro e legível.