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
- Structs, Enums e Pattern Matching — tutorial introdutório sobre o tema
- Closures em Rust — closures frequentemente usam pattern matching nos argumentos
- Trait Objects vs Generics — alternativa a enums para polimorfismo
- Iteradores em Rust — combinando pattern matching com iteradores
- Tratamento de Erros — pattern matching com
ResulteOption