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
- The Rust Programming Language - Traits: Defining Shared Behavior — Capítulo oficial do livro sobre traits, cobrindo definição, implementação e trait bounds
- Rust by Example - Traits — Exemplos práticos de traits, incluindo derivação e trait objects
- Rust Reference - Traits — Documentação de referência detalhada sobre a sintaxe e semântica de traits
- The Rustonomicon - Trait Objects — Discussão avançada sobre trait objects e suas implicações de memória
- Rust Design Patterns - Trait Object Pattern — Padrões de design usando trait objects para polimorfismo em Rust