Pattern matching em Rust
Na página de Match você aprendeu o básico: braços, _, juntar valores com | e intervalos. Isso é só o começo. Um padrão (pattern) em Rust também consegue olhar dentro de um valor, tirar as partes de que você precisa e verificar condições extras, tudo em uma linha só. É isso que chamamos de pattern matching (casamento de padrões).
Pense em um padrão como uma forminha de biscoito. Você pressiona a forma sobre um valor. Se o formato encaixar, o Rust entrega para você os pedaços que estão dentro. Se não encaixar, o Rust tenta o próximo braço. Quando essa ideia fizer sentido, você vai usar padrões em todo lugar: no match, no let, nos parâmetros de funções e no if let.
Match guards: adicionando um if
Seção intitulada “Match guards: adicionando um if”Às vezes o formato não basta e você precisa de uma verificação a mais. Um match guard (guarda) é uma condição if escrita depois do padrão. O braço só é executado se o padrão encaixar e a condição for verdadeira.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "congelante", 0 => "exatamente zero", t if t < 25 => "agradável", _ => "quente", }; println!("{t}°C é {feeling}"); }}Saída:
-5°C é congelante0°C é exatamente zero18°C é agradável35°C é quenteO Rust confere os braços de cima para baixo, então a ordem importa. O 18 não passa no primeiro guard, não é 0 e passa em t < 25, então ele é “agradável”.
Desestruturando tuplas e structs
Seção intitulada “Desestruturando tuplas e structs”Desestruturar (destructuring) significa quebrar um valor em suas partes. Para uma tupla, escreva o padrão no mesmo formato da tupla. Para uma struct, diga o nome dos campos que você quer.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("a origem"), (x, 0) => println!("no eixo x, em {x}"), (0, y) => println!("no eixo y, em {y}"), (x, y) => println!("em outro lugar: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("no eixo y, y = {y}"), Point { x, y: 0 } => println!("no eixo x, x = {x}"), Point { x, y } => println!("em ({x}, {y})"), }}Saída:
em outro lugar: (3, -7)no eixo y, y = 5Repare na mistura. O 0 é um valor fixo, que precisa combinar exatamente. Já x e y são variáveis novas que pegam o que estiver ali. E Point { x, y } é uma forma curta de escrever Point { x: x, y: y }.
Desestruturando enums com dados
Seção intitulada “Desestruturando enums com dados”É aqui que os padrões brilham de verdade. Uma variante de enum pode carregar dados, e um padrão consegue desempacotar esses dados enquanto verifica qual variante você tem.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("área = {:.2}", area(shape)); }}Saída:
área = 12.57área = 13.50área = 2.25Você não consegue chegar ao radius sem antes provar que o valor é um Circle. Essa é a vantagem em segurança: o compilador não deixa você ler dados da variante errada.
Padrões aninhados e bindings com @
Seção intitulada “Padrões aninhados e bindings com @”Padrões podem ficar dentro de outros padrões, na profundidade que você quiser. Aqui, um enum Command guarda um enum Direction, e um único braço combina os dois níveis ao mesmo tempo.
Um binding com @ permite testar um valor contra um padrão e guardá-lo em uma variável. n @ 1..=9 quer dizer “confira se está entre 1 e 9 e chame isso de n”.
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("subir {steps} (passo pequeno)"), Command::Move(Direction::Up, steps) => println!("subir {steps} (pulo grande!)"), Command::Move(Direction::Down, steps) => println!("descer {steps}"), Command::Say(text) => println!("dizendo: {text}"), Command::Quit => println!("tchau"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("olá"))); run(Command::Quit);}Saída:
subir 3 (passo pequeno)subir 50 (pulo grande!)descer 2dizendo: olátchauSem o @, um intervalo como 1..=9 confere o valor, mas o joga fora. Com o @, você fica com ele.
Ignorando partes com _ e ..
Seção intitulada “Ignorando partes com _ e ..”Muitas vezes você só se importa com alguns dos pedaços. Use _ para pular um valor. Use .. para pular “todo o resto”.
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("vermelho = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("primeiro = {first}, último = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} está no nível {level}");}Saída:
vermelho = 255primeiro = 1, último = 5Mira está no nível 7O .. só pode aparecer uma vez em um padrão. (first, .., last) funciona, mas (.., middle, ..) não: o Rust não teria como saber qual valor você quis dizer.
Padrões no let e nos parâmetros de funções
Seção intitulada “Padrões no let e nos parâmetros de funções”Todo let é, no fundo, um padrão. Todo parâmetro de função também. Isso quer dizer que você pode desestruturar um valor bem no lugar em que ele chega.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Saída:
1920x1080x: 4, y: -2[3, 7, 11]A closure |&(a, b)| a + b usa a mesma ideia. Cada item é um &(i32, i32), e o padrão o desempacota direto em a e b. Você vai ver isso bastante com iteradores.
matches! e let-else
Seção intitulada “matches! e let-else”Duas ferramentas práticas poupam você de escrever um match completo.
matches!(valor, padrão) retorna true ou false. É perfeita dentro de um if ou de um .filter() quando você só precisa de uma resposta de sim ou não. Guards também funcionam aqui.
let-else resolve o caso “desempacote isto ou saia agora”. Escreva let PADRÃO = valor else { ... };. Se o padrão encaixar, as variáveis ficam disponíveis no resto da função. Se não encaixar, o bloco else é executado e ele precisa sair: com return, break, continue ou um panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' não é um número"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} aprovados");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("trinta"));}Saída:
true3 aprovadosSome(31)'trinta' não é um númeroNoneO let-else mantém o “caminho feliz” sem aninhamento. Em vez de colocar seu código dentro de um if let ... { }, você trata o caso ruim primeiro e segue em frente. Ele combina muito bem com valores Option e Result.
Erros comuns
Seção intitulada “Erros comuns”Guards não contam para “todos os casos cobertos”. O compilador não consegue analisar as suas condições if, então este código falha, mesmo com os dois guards cobrindo todos os números:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("negativo"), x if x >= 0 => println!("zero ou positivo"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityO compilador está dizendo que o match não é exaustivo: como braços com guard não contam nessa verificação, para ele nenhum valor de i32 está coberto. A solução: faça do último braço um curinga simples, como _ => println!("zero ou positivo").
Um nome de variável em um padrão é uma variável nova, não uma comparação. Parece que este código verifica se guess == expected, mas não verifica:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Acertou!"), _ => println!("Errou"), }}Ele compila e imprime Acertou!, porque o expected no braço só cria uma variável nova que pega qualquer valor. O Rust avisa você:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Acertou!"), | -------- matches any value6 | _ => println!("Errou"), | ^ no value can reach thisO aviso diz que o padrão _ é inalcançável: expected já aceita qualquer valor, então nada chega até ele. A solução é usar um guard: g if g == expected => println!("Acertou!").
Prática
Seção intitulada “Prática”1. FizzBuzz com uma tupla
Seção intitulada “1. FizzBuzz com uma tupla”Imprima os números de 9 a 15, mas escreva Fizz para os múltiplos de 3, Buzz para os múltiplos de 5 e FizzBuzz para os múltiplos dos dois. Faça isso com um único match sobre uma tupla, em vez de uma sequência de if. Dica: faça o match em (n % 3, n % 5). Um resto 0 quer dizer “divide certinho”, e _ quer dizer “tanto faz”.
Ver solução
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Saída:
FizzBuzz11Fizz1314FizzBuzzO (0, 0) precisa vir primeiro. Se o (0, _) viesse antes, o 15 imprimiria Fizz.
2. Transações bancárias
Seção intitulada “2. Transações bancárias”Defina enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Comece com um balance (saldo) de 100 e aplique esta lista, na ordem: depósito de 50, saque de 30, saque de 500, tarifa. A tarifa custa 5. Um saque maior que o saldo precisa ser recusado com uma mensagem. No fim, imprima o saldo. Dica: use um match guard, Transaction::Withdraw(amount) if amount <= balance, seguido de um segundo braço Withdraw para o caso recusado.
Ver solução
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("recusado: não dá para sacar {amount}"), Transaction::Fee => balance -= 5, } }
println!("saldo = {balance}");}Saída:
recusado: não dá para sacar 500saldo = 115O segundo braço Withdraw não tem guard, então ele pega todo saque que o primeiro braço recusou. Sem ele, o match não compilaria: guards não contam quando o Rust verifica se todos os casos foram cobertos.
- Um match guard (
padrão if condição) adiciona uma verificação extra a um braço. - Padrões podem desestruturar tuplas, structs e variantes de enums, e podem ser aninhados.
nome @ padrãotesta um valor e o guarda em uma variável ao mesmo tempo._pula um valor;..pula o resto. Padrões também funcionam nolet, em parâmetros de funções e em closures.matches!dá umtrue/falserápido; o let-else desempacota um valor ou sai mais cedo.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que n @ 1..=9 faz em um braço de match?
2. No let-else, o que o bloco else precisa fazer?
3. O que let (first, .., last) = (1, 2, 3, 4, 5); dá a você?
4. Por que um match só com braços com guard muitas vezes não compila?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Patterns and Matching — The Rust Book (em inglês): o capítulo oficial sobre todos os lugares em que padrões aparecem e quando eles podem não encaixar.
- Pattern Syntax — The Rust Book (em inglês): um passeio por todas as formas de padrão, de intervalos e desestruturação até guards e bindings com
@. - Staying on the Happy Path with let…else — The Rust Book (em inglês): um exemplo tranquilo e completo de let-else.
- Destructuring — Rust by Example (em inglês): exemplos executáveis de desestruturação de tuplas, arrays, enums, ponteiros e structs.
- Macro
matches!— documentação da std (em inglês): a sintaxe exata da macro, inclusive como adicionar um guard.