Pattern matching com match
Nesta aula, você aprenderá a usar o match em Rust para pattern matching exaustivo, com padrões, guardas, catch-all e binding. A aula cobre desde a sintaxe básica até técnicas avançadas para escrever código seguro e expressivo.
O pattern matching é uma das características mais poderosas de Rust, permitindo que você compare um valor contra uma série de padrões e execute código diferente para cada combinação. O match é a principal ferramenta para isso, e ele é exaustivo: você deve cobrir todas as possibilidades, garantindo segurança em tempo de compilação. Nesta aula, exploraremos a fundo o match, incluindo padrões, guardas, o curinga _ e o binding de valores.
Dominar o match é essencial para escrever código idiomático em Rust, especialmente ao trabalhar com enums, opcionais e tipos complexos. Vamos começar com o conceito de exaustividade e depois mergulhar em tópicos avançados.
match exaustivo
Em Rust, o match é exaustivo: você deve cobrir todos os casos possíveis do valor que está sendo analisado. Se você esquecer um caso, o compilador emitirá um erro. Isso elimina bugs comuns de linguagens que não têm essa exigência, como esquecer de tratar um valor nulo ou uma variante de enum.
Por exemplo, considere um enum simples:
enum Cor {
Vermelho,
Verde,
Azul,
}
fn descrever(cor: Cor) -> &'static str {
match cor {
Cor::Vermelho => "vermelho",
Cor::Verde => "verde",
Cor::Azul => "azul",
}
}
Se você remover um dos braços, o compilador rejeitará o código. A exaustividade se aplica a qualquer tipo, incluindo inteiros, chars e booleanos. Para tipos primitivos, você pode usar ranges e listas para cobrir múltiplos valores.
Exemplo com números:
fn classificar_numero(n: i32) -> &'static str {
match n {
0 => "zero",
1..=10 => "pequeno",
11..=100 => "médio",
_ => "grande",
}
}
O padrão _ (catch-all) é útil para cobrir todos os casos restantes. Sem ele, o compilador exigiria que você listasse todos os valores possíveis de i32, o que é inviável.
Padrões e guardas
Além de padrões simples, o match suporta guardas, que são condições adicionais que devem ser verdadeiras para que o braço seja executado. Guardas são escritas com a palavra-chave if após o padrão.
Exemplo com guarda:
fn descrever_numero(n: i32) -> &'static str {
match n {
0 => "zero",
n if n < 0 => "negativo",
n if n > 0 && n <= 10 => "pequeno positivo",
_ => "outro",
}
}
Guardas permitem expressões booleanas arbitrárias, incluindo chamadas de função. Elas são avaliadas apenas se o padrão corresponder primeiro.
Padrões também podem ser combinados com | para representar múltiplos valores:
fn vogal(c: char) -> bool {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => true,
_ => false,
}
}
Você pode usar padrões em outros lugares, como em if let e while let, que são açúcares sintáticos para casos específicos de match.
_ catch-all
O padrão _ é um curinga que corresponde a qualquer valor. Ele é frequentemente usado no último braço do match para cobrir todos os casos não especificados. Além disso, você pode usar _ dentro de padrões para ignorar partes de um valor, como em tuplas ou structs.
Exemplo ignorando um campo de uma tupla:
let tupla = (1, 2, 3);
match tupla {
(x, _, z) => println!("x={}, z={}", x, z),
}
O _ não faz binding: você não pode usar o valor ignorado. Se você quiser usar o valor, mas não se importa com o nome, pode usar _nome (com underline prefixado), que ainda faz binding, mas suprime warnings de variável não usada.
Outro uso comum é em match com enums que têm dados:
enum Mensagem {
Sair,
Mover { x: i32, y: i32 },
Escrever(String),
}
fn processar(msg: Mensagem) {
match msg {
Mensagem::Sair => println!("saindo"),
Mensagem::Mover { x, y } => println!("move para ({}, {})", x, y),
Mensagem::Escrever(_) => println!("escreveu algo"),
}
}
No braço Escrever(_), ignoramos o conteúdo da string. Isso é útil quando queremos tratar a variante sem extrair os dados.
Binding
O binding em match permite que você capture partes do valor correspondente e as vincule a variáveis. Isso é feito automaticamente quando você nomeia os campos ou usa @ para vincular um valor a uma variável enquanto também testa o padrão.
Exemplo básico de binding:
enum Opcao {
Algum(i32),
Nenhum,
}
fn obter_valor(op: Opcao) -> i32 {
match op {
Opcao::Algum(x) => x,
Opcao::Nenhum => 0,
}
}
Aqui, x é vinculado ao valor dentro de Algum. Você também pode usar @ para vincular um valor que também é testado por um padrão:
fn descrever_numero(n: i32) -> &'static str {
match n {
x @ 0..=10 => {
println!("x é {}", x);
"pequeno"
},
_ => "grande",
}
}
O padrão x @ 0..=10 testa se n está no intervalo e, se sim, vincula o valor a x. Isso é útil quando você precisa do valor original dentro do braço.
Binding também funciona com structs e enums com campos nomeados:
struct Ponto {
x: i32,
y: i32,
}
fn origem(p: Ponto) -> bool {
match p {
Ponto { x: 0, y: 0 } => true,
Ponto { x, y } => false, // x e y são vinculados
}
}
Nesse caso, x e y são vinculados aos valores respectivos. Você pode usar .. para ignorar campos restantes.
O binding combinado com if let é uma forma concisa de extrair valores:
if let Some(valor) = opcao {
println!("valor: {}", valor);
}
Isso é equivalente a um match de um braço.
Boas práticas
Prefira match a cadeias de if-else quando você está comparando um valor contra várias alternativas, pois o match é mais legível e o compilador verifica exaustividade. Use guardas para condições complexas que não podem ser expressas apenas com padrões. Evite usar _ indiscriminadamente; sempre que possível, nomeie os casos para maior clareza. Lembre-se de que _ não faz binding, então use _nome se precisar referir-se ao valor.
Referências
- The Rust Programming Language - Match
- Rust by Example - Match
- Rust Reference - Match Expressions
- All the Places Patterns Can Be Used
- Pattern Syntax
Exercícios
- Escreva uma função que recebe um
Option<i32>e retorna o valor interno se forSome, ou 0 se forNone. Usematch. - Dado um enum
Resultadocom variantesSucesso(i32)eErro(String), escreva uma função que retorna uma string descrevendo o resultado. ParaSucesso, retorne "Sucesso: {valor}"; paraErro, retorne "Erro: {mensagem}". - Use um guarda em um
matchpara classificar um número como "positivo", "negativo" ou "zero". - Dada uma tupla
(i32, i32, i32), usematchcom_para ignorar o segundo elemento e imprimir apenas o primeiro e o terceiro. - Escreva uma função que recebe um
Result<i32, String>e, usandomatchcom binding, retorna o dobro do valor se forOk, ou a string de erro concatenada com "!" se forErr.
fn extrair(op: Option<i32>) -> i32 {
match op {
Some(val) => val,
None => 0,
}
}
enum Resultado {
Sucesso(i32),
Erro(String),
}
fn descrever(res: Resultado) -> String {
match res {
Resultado::Sucesso(val) => format!("Sucesso: {}", val),
Resultado::Erro(msg) => format!("Erro: {}", msg),
}
}
fn classificar(n: i32) -> &'static str {
match n {
n if n > 0 => "positivo",
n if n < 0 => "negativo",
_ => "zero",
}
}
fn imprimir_primeiro_terceiro(t: (i32, i32, i32)) {
match t {
(a, _, c) => println!("primeiro: {}, terceiro: {}", a, c),
}
}
fn processar(res: Result<i32, String>) -> String {
match res {
Ok(val) => (val * 2).to_string(),
Err(msg) => format!("{}!", msg),
}
}