Pular para o conteúdo

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.

À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 é congelante
0°C é exatamente zero
18°C é agradável
35°C é quente
▶ Experimente você mesmo

O 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”.

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 = 5
▶ Experimente você mesmo

Repare 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 }.

É 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.25
▶ Experimente você mesmo

Você 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 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 2
dizendo: olá
tchau
▶ Experimente você mesmo

Sem o @, um intervalo como 1..=9 confere o valor, mas o joga fora. Com o @, você fica com ele.

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 = 255
primeiro = 1, último = 5
Mira está no nível 7
▶ Experimente você mesmo

O .. 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.

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:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ Experimente você mesmo

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.

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:

true
3 aprovados
Some(31)
'trinta' não é um número
None
▶ Experimente você mesmo

O 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.

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 exhaustivity

O 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 value
6 | _ => println!("Errou"),
| ^ no value can reach this

O 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!").

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:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Experimente você mesmo

O (0, 0) precisa vir primeiro. Se o (0, _) viesse antes, o 15 imprimiria Fizz.

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 500
saldo = 115
▶ Experimente você mesmo

O 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ão testa um valor e o guarda em uma variável ao mesmo tempo.
  • _ pula um valor; .. pula o resto. Padrões também funcionam no let, em parâmetros de funções e em closures.
  • matches! dá um true/false rápido; o let-else desempacota um valor ou sai mais cedo.

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