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 PartialEq pode ser problemático, pois NaN != NaN).
  • Quando você precisa de otimizações específicas ou controle fino (ex.: implementar Clone manualmente 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 derive para 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 derive com 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

Exercícios

  1. Defina uma struct Livro com campos titulo: String, autor: String e paginas: u32. Use derive para implementar Debug, Clone, PartialEq e Eq. 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);
    }
    
  2. Escreva um enum Cor com variantes Vermelho, Verde e Azul. Derive Debug e PartialEq. 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);
    }
    
  3. Explique por que derive(PartialEq) pode não ser adequado para um tipo que contém um campo f64. Dê um exemplo e mostre como implementar PartialEq manualmente com uma tolerância.

    ✓ Resposta:

    O PartialEq derivado 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);
    }
    
  4. 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,
    }
    
  5. Escreva uma macro procedural simples que derive um trait Descricao para uma struct, onde o trait tem um método descricao() que retorna uma string com o nome da struct. Use syn e quote. (Dica: você pode criar um projeto separado com proc-macro = true e testar em um crate de exemplo.)

    ✓ Resposta:

    Primeiro, crie uma crate chamada minha_macro com Cargo.toml contendo [lib] proc-macro = true e dependências syn e quote. No lib.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());
    }