Derive macros e atributos
Esta aula explora as derive macros e atributos em Rust, mostrando como usar #[derive] para implementar traits automaticamente, os atributos mais comuns, quando aplicar cada um e uma introdução às macros procedurais. O conteúdo inclui exemplos práticos, boas práticas e exercícios para fixação.
Bem-vindo à aula sobre derive macros e atributos em Rust. Nesta aula, vamos mergulhar em duas ferramentas essenciais que automatizam a implementação de código repetitivo e permitem adicionar metadados às declarações. As derive macros são uma forma de geração de código em tempo de compilação, enquanto os atributos são anotações que modificam o comportamento do compilador ou de macros.
Dominar esses recursos é fundamental para escrever código idiomático e produtivo em Rust, pois eles reduzem a quantidade de código manual e garantem consistência. Vamos começar explorando o atributo #[derive], depois os atributos mais comuns, quando usá-los e, por fim, uma visão geral das macros procedurais, que são a base das derive macros.
#[derive]
O atributo #[derive] é usado em structs, enums e unions para implementar automaticamente um ou mais traits fornecidos pelo compilador ou por bibliotecas externas. É uma das formas mais comuns de usar macros procedurais no Rust, pois o compilador gera a implementação do trait com base na definição do tipo.
Por exemplo, para habilitar a comparação de igualdade, clonagem, depuração e cópia, você pode simplesmente anotar seu tipo com #[derive(PartialEq, Clone, Debug, Copy)]. Isso evita escrever manualmente as implementações, que muitas vezes são mecânicas e propensas a erros.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Ponto {
x: f64,
y: f64,
}
fn main() {
let p1 = Ponto { x: 1.0, y: 2.0 };
let p2 = p1; // Copy
let p3 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq
}Os traits mais comuns que podem ser derivados incluem Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, e Serialize/Deserialize (via serde). É importante notar que nem todos os traits podem ser derivados; por exemplo, Display não pode ser derivado automaticamente porque requer formatação personalizada.
Atributos comuns
Além de #[derive], Rust possui vários atributos que controlam o comportamento do compilador, como #[allow], #[warn], #[deny], #[cfg], #[inline], e #[deprecated]. Esses atributos são usados para ajustar avisos, compilação condicional, otimizações e documentação.
Por exemplo, #[allow(dead_code)] suprime avisos sobre código não utilizado, enquanto #[cfg(target_os = "linux")] inclui o item apenas em sistemas Linux. O atributo #[inline] sugere ao compilador que a função deve ser expandida inline, e #[deprecated] marca um item como obsoleto, gerando avisos para quem o usar.
#[allow(dead_code)]
fn funcao_nao_usada() {}
#[cfg(target_os = "linux")]
fn so_linux() {
println!("Estamos no Linux");
}
#[deprecated(note = "Use a nova função")]
fn antiga() {}
fn main() {
// antiga(); // gera aviso de depreciação
}Outros atributos importantes são #[derive] (já visto), #[repr] para controlar a representação em memória, e #[macro_use] para importar macros. Também existem atributos específicos de crates, como #[serde(rename_all = "camelCase")] usado com a crate serde para personalizar a serialização.
Quando usar
Usar #[derive] é recomendado sempre que você precisa implementar um trait que é puramente mecânico e baseado nos campos do tipo. Por exemplo, se você precisa comparar instâncias de uma struct por igualdade, derivar PartialEq é muito mais simples e menos propenso a erros do que escrever manualmente a implementação.
No entanto, nem sempre é apropriado. Se a implementação do trait requer lógica que não pode ser deduzida automaticamente, como Display ou From, você deve implementá-la manualmente. Além disso, traits como Default só podem ser derivados se todos os campos tiverem um valor padrão, o que nem sempre é o caso.
Uma boa prática é usar derive para traits simples e comuns, e reservar implementações manuais para comportamentos específicos. Isso mantém o código limpo e reduz a duplicação, mas também é importante não exagerar: se o tipo tem invariantes que precisam ser mantidos, a derivação pode quebrá-los silenciosamente.
Visão de macros procedurais
As derive macros são um tipo de macro procedural, que são funções que operam no AST (Árvore Sintática Abstrata) do código Rust e geram novo código. Elas são definidas em crates separadas e podem ser usadas por outras crates. Além de derive, existem macros procedurais de atributo e de função.
Uma macro procedural é escrita em Rust e usa o crate proc_macro para manipular tokens. Para criar uma derive macro, você define uma função anotada com #[proc_macro_derive] que recebe um TokenStream de entrada e retorna um TokenStream de saída. O código gerado é inserido no ponto onde a macro é usada.
// Dentro de uma crate de macros (ex: my_macros)
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MeuTrait)]
pub fn derive_meu_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let nome = &input.ident;
let expanded = quote! {
impl MeuTrait for #nome {
fn metodo() { println!("Implementado automaticamente"); }
}
};
expanded.into()
}Esse exemplo usa as crates syn e quote para parsear e gerar código. Embora as macros procedurais sejam poderosas, elas são avançadas e devem ser usadas com cuidado, pois podem tornar o código mais difícil de ler e depurar. Elas são a base de bibliotecas como serde, diesel e rocket.
Boas práticas e observações finais
Algumas boas práticas ao usar derive macros e atributos:
- Prefira derivar traits padrão quando possível, em vez de implementar manualmente, para reduzir código e erros.
- Use atributos como
#[allow]e#[cfg]com moderação, para não esconder problemas reais. - Ao criar macros procedurais, documente bem e forneça testes, pois elas afetam a compilação de quem as usa.
- Mantenha as implementações manuais de traits quando a lógica for específica, e não tente forçar a derivação.
- Fique atento à compatibilidade: algumas derivações podem não funcionar com tipos genéricos ou com lifetimes complexos.
Essas ferramentas são essenciais no ecossistema Rust, e dominá-las eleva a qualidade e a manutenibilidade do seu código.
Referências
- Rust by Example: Derive
- The Rust Reference: Attributes
- The Rust Reference: Procedural Macros
- The Rust Book: Macros
- Serde: Derive
- syn crate documentation
- quote crate documentation
Exercícios
- Crie uma struct
Pessoacom camposnome(String) eidade(u32). Derive os traitsDebug,Clone,PartialEqeEq. Escreva um programa que crie duas instâncias, clone uma, compare e imprima com{:?}. - Explique por que
Displaynão pode ser derivado automaticamente e dê um exemplo de implementação manual para a structPessoa. - Use o atributo
#[cfg]para criar uma função que só compila em sistemas Windows e outra que só compila em sistemas não-Windows. Imprima uma mensagem apropriada em cada. - Pesquise e descreva o que são macros procedurais de atributo e dê um exemplo de uso (pode ser fictício) para um atributo
#[route]que seria usado em um framework web. - Crie uma macro procedural simples (em um crate separado) que derive um trait
Olapara qualquer struct, onde o trait tem um métodoola()que imprime "Olá, mundo!". Use as cratessynequote. (Dica: configure um crate de macros e um crate de teste).
#[derive(Debug, Clone, PartialEq, Eq)]
struct Pessoa {
nome: String,
idade: u32,
}
fn main() {
let p1 = Pessoa { nome: "Ana".to_string(), idade: 30 };
let p2 = p1.clone();
println!("{:?}", p1);
println!("Iguais? {}", p1 == p2);
}use std::fmt;
impl fmt::Display for Pessoa {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} anos)", self.nome, self.idade)
}
}#[cfg(target_os = "windows")]
fn mensagem() {
println!("Você está no Windows");
}
#[cfg(not(target_os = "windows"))]
fn mensagem() {
println!("Você não está no Windows");
}
fn main() {
mensagem();
}#[route(GET, "/home")]
fn home() { ... }A macro geraria código para registrar a rota em um framework web. Ela é definida com #[proc_macro_attribute] e recebe dois TokenStreams: um para os argumentos do atributo e outro para o item.Cargo.toml da crate de macros:[lib]
proc-macro = true
Defina a macro:use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Ola)]
pub fn derive_ola(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let nome = &input.ident;
let expanded = quote! {
impl Ola for #nome {
fn ola() {
println!("Olá, mundo!");
}
}
};
expanded.into()
}
Na crate de teste, use #[derive(Ola)] e chame Ola::ola().