Introdução
Traits são o coração do sistema de tipos do Rust. Se você vem de outras linguagens, pode pensar nelas como interfaces (Java/Go) ou type classes (Haskell) — mas traits em Rust vão além, habilitando polimorfismo, abstração e composição de comportamentos com segurança em tempo de compilação.
Neste guia, vamos do básico ao avançado: definir traits, implementar para tipos próprios e externos, usar trait bounds com generics, entender trait objects vs generics, e dominar as traits mais importantes da standard library.
Definindo e Implementando Traits
Uma trait define um conjunto de métodos que um tipo deve implementar:
trait Resumo {
fn resumir(&self) -> String;
}
struct Artigo {
titulo: String,
autor: String,
conteudo: String,
}
impl Resumo for Artigo {
fn resumir(&self) -> String {
format!("{}, por {} — {}...", self.titulo, self.autor, &self.conteudo[..80])
}
}
struct Tweet {
usuario: String,
texto: String,
}
impl Resumo for Tweet {
fn resumir(&self) -> String {
format!("@{}: {}", self.usuario, self.texto)
}
}
Agora, qualquer função que aceite algo que implemente Resumo funciona com ambos os tipos:
fn exibir_resumo(item: &impl Resumo) {
println!("Novo conteúdo: {}", item.resumir());
}
fn main() {
let artigo = Artigo {
titulo: "Rust em 2026".into(),
autor: "Comunidade".into(),
conteudo: "Rust continua crescendo em adoção global com novos casos de uso...".repeat(3),
};
let tweet = Tweet {
usuario: "rustlang_br".into(),
texto: "Traits são incríveis!".into(),
};
exibir_resumo(&artigo);
exibir_resumo(&tweet);
}
Implementações Padrão (Default)
Traits podem fornecer implementações padrão que os tipos podem usar ou sobrescrever:
trait Notificacao {
fn destinatario(&self) -> &str;
fn conteudo(&self) -> &str;
// Implementação padrão
fn enviar(&self) -> String {
format!(
"Enviando para {}: {}",
self.destinatario(),
self.conteudo()
)
}
}
struct Email {
para: String,
assunto: String,
corpo: String,
}
impl Notificacao for Email {
fn destinatario(&self) -> &str {
&self.para
}
fn conteudo(&self) -> &str {
&self.corpo
}
// Sobrescreve a implementação padrão
fn enviar(&self) -> String {
format!(
"📧 Email para {}: [{}] {}",
self.para, self.assunto, self.corpo
)
}
}
struct SMS {
numero: String,
mensagem: String,
}
impl Notificacao for SMS {
fn destinatario(&self) -> &str {
&self.numero
}
fn conteudo(&self) -> &str {
&self.mensagem
}
// Usa a implementação padrão de enviar()
}
Implementações padrão são poderosas porque permitem adicionar novos métodos a uma trait sem quebrar implementações existentes — um padrão muito usado na standard library.
Trait Bounds: Restringindo Generics
Trait bounds permitem especificar que um tipo genérico deve implementar certas traits. Existem três sintaxes equivalentes:
// Sintaxe 1: impl Trait (mais concisa)
fn notificar(item: &impl Resumo) {
println!("{}", item.resumir());
}
// Sintaxe 2: Trait bound explícito (mais flexível)
fn notificar<T: Resumo>(item: &T) {
println!("{}", item.resumir());
}
// Sintaxe 3: where clause (melhor para múltiplos bounds)
fn processar<T>(item: &T) -> String
where
T: Resumo + std::fmt::Display,
{
format!("Display: {} | Resumo: {}", item, item.resumir())
}
A cláusula where é especialmente útil quando você tem múltiplos parâmetros genéricos com bounds complexos:
fn comparar_e_resumir<T, U>(a: &T, b: &U) -> String
where
T: Resumo + PartialOrd,
U: Resumo + Clone,
{
format!("A: {} | B: {}", a.resumir(), b.resumir())
}
Trait Objects vs Generics: Quando Usar Cada Um
Esta é uma das decisões mais importantes em Rust. Ambos habilitam polimorfismo, mas com trade-offs diferentes.
Generics (Dispatch Estático)
Com generics, o compilador gera código específico para cada tipo concreto (monomorphization):
fn maior_resumo<T: Resumo>(items: &[T]) -> &T {
// Todos os items devem ser do MESMO tipo
let mut maior = &items[0];
for item in &items[1..] {
if item.resumir().len() > maior.resumir().len() {
maior = item;
}
}
maior
}
Vantagens: performance máxima (sem overhead), inline possível, otimizações do compilador. Desvantagens: tamanho do binário aumenta com cada tipo concreto, todos os itens devem ser do mesmo tipo.
Trait Objects (Dispatch Dinâmico)
Com dyn Trait, a decisão de qual método chamar acontece em runtime via vtable:
fn exibir_todos(items: &[Box<dyn Resumo>]) {
// Cada item pode ser um tipo DIFERENTE
for item in items {
println!("{}", item.resumir());
}
}
fn main() {
let items: Vec<Box<dyn Resumo>> = vec![
Box::new(Artigo {
titulo: "Rust 2026".into(),
autor: "Equipe".into(),
conteudo: "Novidades do ano...".repeat(10),
}),
Box::new(Tweet {
usuario: "dev_br".into(),
texto: "Traits são poderosas!".into(),
}),
];
exibir_todos(&items);
}
Vantagens: coleções heterogêneas, binário menor, mais flexibilidade. Desvantagens: overhead de indireção (vtable lookup), sem inline, trait deve ser object-safe.
Para um aprofundamento nesse tema, veja nosso artigo sobre trait objects vs generics.
Regra Prática
| Cenário | Use |
|---|---|
| Performance crítica | Generics |
| Coleção de tipos diferentes | Trait objects |
| Bibliotecas públicas | Generics (mais flexível) |
| Plugins/extensões | Trait objects |
| Poucos tipos concretos | Generics |
| Muitos tipos concretos | Trait objects (menor binário) |
Traits Essenciais da Standard Library
Rust tem traits fundamentais que aparecem em quase todo código. Conhecê-las é essencial.
Display e Debug
Display e Debug controlam como um tipo é exibido:
use std::fmt;
struct Ponto {
x: f64,
y: f64,
}
// Debug pode ser derivado automaticamente
// #[derive(Debug)]
// Display deve ser implementado manualmente
impl fmt::Display for Ponto {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl fmt::Debug for Ponto {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Ponto {{ x: {}, y: {} }}", self.x, self.y)
}
}
Clone e Copy
Clone e Copy definem como valores são duplicados:
#[derive(Clone, Copy, Debug)]
struct Vetor2D {
x: f64,
y: f64,
}
#[derive(Clone, Debug)]
struct Matriz {
dados: Vec<Vec<f64>>, // Vec não é Copy, então Matriz também não pode ser
}
fn main() {
let v1 = Vetor2D { x: 1.0, y: 2.0 };
let v2 = v1; // Copy implícito — v1 ainda é válido
let v3 = v1; // Funciona porque Vetor2D é Copy
let m1 = Matriz { dados: vec![vec![1.0]] };
let m2 = m1.clone(); // Clone explícito necessário
// let m3 = m1; // ERRO: m1 foi movido se não for Clone
}
From e Into
From e Into permitem conversões entre tipos:
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
// Into é implementado automaticamente quando From existe
fn aquecer(temp: impl Into<Fahrenheit>) {
let f: Fahrenheit = temp.into();
println!("Temperatura: {}°F", f.0);
}
fn main() {
aquecer(Celsius(100.0)); // Converte automaticamente
aquecer(Fahrenheit(212.0)); // Já é Fahrenheit
}
Iterator
Iterator é talvez a trait mais usada em Rust. Implementá-la desbloqueia dezenas de métodos gratuitos:
struct Fibonacci {
atual: u64,
proximo: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { atual: 0, proximo: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let resultado = self.atual;
self.atual = self.proximo;
self.proximo = resultado + self.proximo;
Some(resultado)
}
}
fn main() {
// Todos esses métodos vêm de graça ao implementar next()
let soma: u64 = Fibonacci::new()
.take(20)
.filter(|n| n % 2 == 0)
.sum();
println!("Soma dos 20 primeiros Fibonacci pares: {}", soma);
let primeiros: Vec<u64> = Fibonacci::new().take(10).collect();
println!("Primeiros 10: {:?}", primeiros);
}
Para mais sobre iteradores, veja nosso artigo sobre iteradores em Rust.
Supertraits: Composição de Traits
Uma trait pode exigir que outra trait já esteja implementada:
use std::fmt;
// Imprimivel exige que Display já esteja implementado
trait Imprimivel: fmt::Display {
fn imprimir(&self) {
println!("[LOG] {}", self);
}
fn imprimir_com_borda(&self) {
let texto = format!("{}", self);
let borda = "═".repeat(texto.len() + 4);
println!("╔{}╗", borda);
println!("║ {} ║", texto);
println!("╚{}╝", borda);
}
}
// Qualquer tipo que implemente Display pode implementar Imprimivel
impl Imprimivel for Ponto {}
Blanket Implementations
Você pode implementar uma trait para todos os tipos que satisfaçam uma condição:
trait Logavel {
fn log(&self);
}
// Implementa Logavel para QUALQUER tipo que implemente Display
impl<T: fmt::Display> Logavel for T {
fn log(&self) {
println!("[{}] {}", chrono::Local::now().format("%H:%M:%S"), self);
}
}
A standard library usa esse padrão extensivamente. Por exemplo, Into<U> é implementado automaticamente para qualquer T que implemente From<T>.
Conclusão
Traits são o mecanismo central de abstração em Rust. Dominar traits significa dominar Rust. Recapitulando os pontos principais:
- Traits definem comportamentos que tipos podem implementar
- Implementações padrão evitam código repetitivo
- Trait bounds restringem generics de forma segura
- Generics dão performance máxima; trait objects dão flexibilidade
- Traits da stdlib como Display, Clone, From e Iterator aparecem em todo código Rust
Para se aprofundar, explore nossos artigos sobre closures (que usam traits Fn, FnMut e FnOnce), smart pointers (que dependem das traits Deref e Drop), e pattern matching avançado. Se está começando com Rust, nosso tutorial de traits e generics é um ótimo ponto de partida.
Traits não são apenas um recurso da linguagem — são a filosofia do Rust em ação: composição sobre herança, segurança sobre conveniência, e abstração sem custo em runtime.
Se você trabalha com múltiplas linguagens, vale comparar como cada uma resolve abstração: Python usa duck typing e protocolos, enquanto Kotlin oferece interfaces com implementações padrão semelhantes às traits do Rust.