Derive macros e atributos
Nesta aula, você aprenderá a usar a macro derive para implementar automaticamente traits comuns em Rust, explorará atributos como #[derive], #[allow], #[inline] e outros, entenderá quando usar derive em vez de implementações manuais e terá uma visão geral das macros procedurais que permitem criar seus próprios derives.
Em Rust, a macro derive é uma ferramenta poderosa que permite implementar automaticamente traits comuns para seus tipos, economizando tempo e reduzindo erros. Em vez de escrever manualmente implementações de traits como Debug, Clone ou PartialEq, você pode simplesmente anotar uma struct ou enum com #[derive(...)] e o compilador gera o código necessário. Esta aula explora em profundidade o uso de derive, os atributos que o acompanham, e como decidir quando usá-lo. Além disso, você terá uma introdução às macros procedurais, que são a base para criar seus próprios derives personalizados.
Dominar derive é essencial para escrever código idiomático em Rust, pois muitos tipos de dados precisam de implementações padrão para funcionar bem com a linguagem e o ecossistema. Nesta aula, você aprenderá não apenas o mecanismo, mas também as boas práticas e os casos em que a implementação manual é preferível.
#[derive]
A macro derive é um atributo que pode ser aplicado a structs, enums e unions. Ela instrui o compilador a gerar automaticamente implementações de um ou mais traits especificados entre parênteses. Os traits mais comuns suportados são: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize (via serde) e Deserialize (via serde).
O derive funciona gerando uma implementação que segue regras simples: para Debug, ele imprime os campos; para Clone, chama clone em cada campo; para PartialEq, compara campo a campo; e assim por diante. Isso é possível porque o compilador tem acesso à estrutura do tipo. Por exemplo, considere a seguinte struct:
#[derive(Debug, Clone, PartialEq)]
struct Ponto {
x: i32,
y: i32,
}
fn main() {
let p1 = Ponto { x: 1, y: 2 };
let p2 = p1.clone();
println!("{:?}", p1);
assert_eq!(p1, p2);
}
Sem o derive, você teria que escrever manualmente as implementações de Debug, Clone e PartialEq. O derive gera exatamente o código que você escreveria, mas sem erro humano. É importante notar que derive só funciona se todos os campos do tipo também implementarem o trait (ou forem de tipos primitivos). Caso contrário, o compilador emitirá um erro.
Além dos traits padrão, você pode usar derive com traits de bibliotecas externas, como serde::Serialize e serde::Deserialize, desde que a biblioteca forneça a macro procedural correspondente. Isso é feito adicionando #[derive(Serialize, Deserialize)] e importando a crate serde com o recurso derive.
Atributos comuns
Além de #[derive], Rust possui muitos outros atributos que controlam o comportamento do compilador, otimização, lint e muito mais. Alguns dos mais comuns são:
#[allow(...)]e#[deny(...)]: controlam lints do compilador, permitindo ou negando avisos específicos.#[inline]e#[inline(always)]: sugerem ao compilador que a função deve ser inline.#[cfg(...)]: compilação condicional, usada para incluir ou excluir código baseado em configurações.#[deprecated]: marca um item como obsoleto, gerando aviso de deprecação.#[must_use]: indica que o valor retornado deve ser usado.#[repr(...)]: controla o layout de memória de structs e enums.#[derive(...)]já mencionado.
Esses atributos são aplicados a itens como funções, structs, módulos, etc. Por exemplo, para suprimir um aviso de variável não utilizada, você pode usar #[allow(dead_code)]:
#[allow(dead_code)]
fn funcao_nao_usada() {
println!("Não usada");
}
Atributos podem ser combinados e alguns possuem parâmetros. Por exemplo, #[cfg(target_os = "linux")] faz com que o item seja compilado apenas em Linux. O atributo #[repr(C)] é útil para garantir uma representação de memória compatível com C, importante para FFI.
É essencial conhecer esses atributos para escrever código eficiente e sem avisos desnecessários. Eles também são usados em macros procedurais e no desenvolvimento de bibliotecas.
Quando usar
O derive deve ser usado sempre que você precisar de uma implementação padrão de um trait e não houver necessidade de personalização. Por exemplo, para tipos simples de dados como coordenadas, cores ou configurações, derive(Debug, Clone, Copy, PartialEq, Eq, Hash) é perfeito. Isso reduz a verbosidade e evita erros.
No entanto, há casos em que você deve evitar derive e escrever a implementação manualmente:
- Quando a implementação padrão não é correta para a lógica do tipo (ex.: comparar floats com
PartialEqpode ser problemático, poisNaN != NaN). - Quando você precisa de otimizações específicas ou controle fino (ex.: implementar
Clonemanualmente para evitar clonagem desnecessária de campos grandes). - Quando o tipo contém campos que não implementam o trait, e você precisa de lógica especial para eles.
- Quando você quer implementar um trait que não é suportado por
derive(ex.:Display,FromStr).
Por exemplo, para um tipo que representa uma temperatura em Celsius, você pode querer implementar PartialEq manualmente para comparar com uma tolerância:
struct Temperatura(f64);
impl PartialEq for Temperatura {
fn eq(&self, other: &Self) -> bool {
(self.0 - other.0).abs() < 0.001
}
}
Outro caso é quando você tem um enum com variantes que não podem ser comparadas diretamente porque contêm funções ou closures. Nesses casos, derive falharia.
Em geral, a regra é: use derive para implementações simples e mecânicas, e implemente manualmente quando houver lógica de negócio ou requisitos especiais.
Visão de macros procedurais
As macros procedurais são funções que recebem código Rust como entrada e produzem código Rust como saída. Elas são usadas para implementar derive personalizados, atributos e macros similares a funções. Diferente das macros declarativas (macro_rules!), que trabalham com padrões de tokens, as macros procedurais operam na AST (Árvore Sintática Abstrata) e podem realizar transformações complexas.
Existem três tipos de macros procedurais: derive, attribute e function-like. A macro derive é a que estamos estudando: ela permite adicionar novos traits deriváveis. Para criar uma macro procedural, você precisa definir uma função marcada com #[proc_macro_derive] e usar uma crate auxiliar chamada syn e quote para analisar e gerar código.
Um exemplo simples de uma macro derive personalizada que implementa um trait Hello:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = quote! {
impl Hello for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
expanded.into()
}
Para usar essa macro, você precisa criar uma crate separada para as macros (geralmente chamada de nome_macro) e depois referenciá-la no seu crate principal. O processo envolve configurar o Cargo.toml com proc-macro = true.
As macros procedurais são avançadas e exigem conhecimento de AST e geração de código. Elas são usadas em bibliotecas populares como serde, diesel e wasm-bindgen. Entender como elas funcionam lhe dá a capacidade de criar abstrações poderosas e reutilizáveis.
Boas práticas e observações finais
Ao usar derive, tenha em mente:
- Use
derivepara traits padrão sempre que possível, mas não exagere: derivar muitos traits pode aumentar o tamanho do binário, mas em geral é inofensivo. - Combine
derivecom atributos como#[serde(rename_all = "camelCase")]para personalizar a serialização sem escrever código manual. - Para tipos com campos não triviais, considere implementar manualmente para evitar surpresas.
- Atributos como
#[allow]devem ser usados com moderação e sempre com justificativa.
As macros procedurais são um tópico avançado; comece com derives padrão e, quando se sentir confortável, explore a criação de suas próprias macros para automatizar padrões repetitivos.
Referências
- Rust Reference - Derive
- The Rust Book - Deriving Traits
- Rust Reference - Attributes
- The Rust Book - Macros
- Serde Derive Documentation
- Rust Reference - Procedural Macros
Exercícios
Defina uma struct
Livrocom campostitulo: String,autor: Stringepaginas: u32. Usederivepara implementarDebug,Clone,PartialEqeEq. Crie dois livros, compare-os e imprima-os.✓ Resposta:#[derive(Debug, Clone, PartialEq, Eq)] struct Livro { titulo: String, autor: String, paginas: u32, } fn main() { let livro1 = Livro { titulo: "O Senhor dos Anéis".to_string(), autor: "J.R.R. Tolkien".to_string(), paginas: 1200 }; let livro2 = livro1.clone(); println!("{:?}", livro1); assert_eq!(livro1, livro2); }Escreva um enum
Corcom variantesVermelho,VerdeeAzul. DeriveDebugePartialEq. Crie duas cores e verifique se são iguais.✓ Resposta:#[derive(Debug, PartialEq)] enum Cor { Vermelho, Verde, Azul, } fn main() { let c1 = Cor::Vermelho; let c2 = Cor::Vermelho; assert_eq!(c1, c2); println!("{:?} é igual a {:?}", c1, c2); }Explique por que
derive(PartialEq)pode não ser adequado para um tipo que contém um campof64. Dê um exemplo e mostre como implementarPartialEqmanualmente com uma tolerância.✓ Resposta:O
PartialEqderivado compara valores de ponto flutuante com igualdade exata, o que pode falhar devido a erros de precisão (ex.:0.1 + 0.2 != 0.3). Para comparações com tolerância, implemente manualmente:struct Medida { valor: f64, } impl PartialEq for Medida { fn eq(&self, other: &Self) -> bool { (self.valor - other.valor).abs() < 1e-9 } } fn main() { let m1 = Medida { valor: 0.1 + 0.2 }; let m2 = Medida { valor: 0.3 }; assert!(m1 == m2); }Pesquise e descreva o que faz o atributo
#[repr(C)]. Em que cenário ele é útil? Dê um exemplo de struct com esse atributo.✓ Resposta:#[repr(C)]define que a struct deve seguir a representação de memória da linguagem C, ou seja, os campos são dispostos na ordem declarada, sem reordenação para alinhamento, e com tamanhos e alinhamentos compatíveis com C. Isso é essencial para interoperabilidade com código C (FFI). Exemplo:#[repr(C)] struct Dados { id: u32, nome: [u8; 32], ativo: bool, }Escreva uma macro procedural simples que derive um trait
Descricaopara uma struct, onde o trait tem um métododescricao()que retorna uma string com o nome da struct. Usesynequote. (Dica: você pode criar um projeto separado comproc-macro = truee testar em um crate de exemplo.)✓ Resposta:Primeiro, crie uma crate chamada
minha_macrocomCargo.tomlcontendo[lib] proc-macro = truee dependênciassynequote. Nolib.rs:use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(Descricao)] pub fn derive_descricao(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let expanded = quote! { impl Descricao for #name { fn descricao() -> String { format!("Struct {}", stringify!(#name)) } } }; expanded.into() }No seu crate principal, adicione a dependência da macro e use:
use minha_macro::Descricao; trait Descricao { fn descricao() -> String; } #[derive(Descricao)] struct Pessoa; fn main() { println!("{}", Pessoa::descricao()); }