Traits: comportamento compartilhado

1. O que são Traits e por que usá-los?

Em Rust, traits são contratos de comportamento que definem um conjunto de métodos que um tipo deve implementar. Eles representam a principal forma de alcançar polimorfismo na linguagem, sem recorrer à herança tradicional de classes.

Pense em um trait como uma interface em Java ou Go, mas com capacidades mais flexíveis. Um trait define "o que" um tipo pode fazer, enquanto a implementação define "como" ele faz. Isso permite que tipos diferentes compartilhem o mesmo comportamento sem precisar compartilhar uma hierarquia de herança.

Os benefícios incluem:
- Polimorfismo sem herança: tipos não relacionados podem implementar o mesmo trait
- Reuso de código: métodos padrão podem ser fornecidos no próprio trait
- Composição sobre herança: um tipo pode implementar múltiplos traits

2. Definindo e implementando Traits

A sintaxe básica para definir um trait é direta:

trait Summary {
    fn summarize(&self) -> String;
}

Para implementar este trait em um tipo:

struct Article {
    title: String,
    author: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} por {}", self.title, self.author)
    }
}

struct Tweet {
    username: String,
    message: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.message)
    }
}

Podemos também implementar traits para tipos da biblioteca padrão:

impl Summary for Vec<i32> {
    fn summarize(&self) -> String {
        format!("Vetor com {} elementos", self.len())
    }
}

3. Métodos com implementação padrão

Traits podem fornecer implementações padrão para seus métodos, permitindo que tipos implementem apenas os métodos que precisam ser customizados:

trait Summarizable {
    fn summarize(&self) -> String {
        String::from("(Sem resumo disponível)")
    }

    fn author_summary(&self) -> String;
}

struct DefaultArticle {
    title: String,
}

impl Summarizable for DefaultArticle {
    fn summarize(&self) -> String {
        format!("Artigo: {}", self.title)
    }

    fn author_summary(&self) -> String {
        String::from("Autor desconhecido")
    }
}

struct SimpleNote;

impl Summarizable for SimpleNote {
    // Usa o summarize padrão
    fn author_summary(&self) -> String {
        String::from("Nota pessoal")
    }
}

4. Traits como parâmetros de funções (trait bounds)

Podemos usar traits como restrições para parâmetros genéricos:

fn notify<T: Summary>(item: &T) {
    println!("Notícia: {}", item.summarize());
}

Para múltiplos bounds, usamos +:

use std::fmt::Display;

fn print_summary<T: Summary + Display>(item: &T) {
    println!("{}", item);
    println!("Resumo: {}", item.summarize());
}

Quando a assinatura fica complexa, a sintaxe where melhora a legibilidade:

fn complex_function<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Display,
    U: Summary + Clone,
{
    format!("{} - {}", t.summarize(), u.summarize())
}

5. Tipos associados a Traits

Traits podem definir tipos placeholder que serão especificados na implementação:

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

A diferença principal entre tipos associados e genéricos é que um tipo só pode ter uma implementação de um trait com tipo associado, enquanto poderia ter múltiplas implementações com genéricos diferentes.

6. Derivação automática de Traits

Rust pode implementar automaticamente certos traits usando #[derive]:

#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
println!("{:?} == {:?}: {}", p1, p2, p1 == p2);

Limitações importantes:
- Copy só pode ser derivado se todos os campos implementarem Copy
- Clone requer que todos os campos implementem Clone
- Tipos com referências ou ponteiros inteligentes podem não suportar derivação automática

Quando a derivação automática não é possível ou desejada, implementamos manualmente:

struct UniqueId {
    id: u64,
    label: String,
}

impl PartialEq for UniqueId {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id  // Compara apenas pelo ID
    }
}

7. Traits e genéricos avançados

Trait objects permitem polimorfismo dinâmico usando dyn Trait:

fn get_summarizer(choice: bool) -> Box<dyn Summary> {
    if choice {
        Box::new(Article {
            title: String::from("Rust"),
            author: String::from("João"),
            content: String::from("Conteúdo..."),
        })
    } else {
        Box::new(Tweet {
            username: String::from("@rustacean"),
            message: String::from("Aprendendo traits!"),
        })
    }
}

A sintaxe impl Trait oferece uma forma concisa para parâmetros:

fn notify_impl(item: &impl Summary) {
    println!("{}", item.summarize());
}

Comparação entre abordagens:

Característica Genéricos (T: Trait) Trait Objects (dyn Trait)
Dispatch Estático (monomorfização) Dinâmico (vtable)
Performance Mais rápido Overhead pequeno
Tamanho do binário Maior (código duplicado) Menor
Flexibilidade Tipos conhecidos em compile-time Tipos determinados em runtime

8. Boas práticas e exemplos do mundo real

A biblioteca padrão do Rust utiliza extensivamente traits. Display para formatação amigável, Debug para depuração, Clone para duplicação explícita e Copy para cópia de bits.

Exemplo completo: sistema de notificações

use std::fmt::Display;

trait Notifiable {
    fn message(&self) -> String;
    fn priority(&self) -> u8;
}

struct Email {
    to: String,
    subject: String,
    body: String,
}

impl Notifiable for Email {
    fn message(&self) -> String {
        format!("Email para {}: {}", self.to, self.subject)
    }

    fn priority(&self) -> u8 {
        5
    }
}

struct SMS {
    number: String,
    text: String,
}

impl Notifiable for SMS {
    fn message(&self) -> String {
        format!("SMS para {}: {}", self.number, self.text)
    }

    fn priority(&self) -> u8 {
        8
    }
}

fn send_notification<T: Notifiable + Display>(notification: &T) {
    println!("Enviando: {}", notification);
    if notification.priority() > 7 {
        println!("⚠️ Alta prioridade!");
    }
}

fn main() {
    let email = Email {
        to: String::from("user@example.com"),
        subject: String::from("Bem-vindo!"),
        body: String::from("Conteúdo do email"),
    };

    let sms = SMS {
        number: String::from("+5511999999999"),
        text: String::from("Seu código é 1234"),
    };

    send_notification(&email);
    send_notification(&sms);
}

Quando criar um trait vs. usar funções soltas:
- Crie um trait quando múltiplos tipos precisam implementar o mesmo comportamento
- Use funções soltas para operações específicas de um único tipo
- Traits são ideais para definir APIs extensíveis e testáveis

A escolha entre implementação manual e derivação automática depende do controle necessário: derivação para casos padrão, implementação manual para lógica customizada.

Referências