👋 Bem-vindos, desbravadores! Se você vem do Python e está começando sua jornada em Rust, provavelmente já se deparou com o conceito de traits. Talvez você tenha pensado: “Isso parece com herança de classes do Python, mas não exatamente…”
Neste post, vamos explorar como traits em Rust oferecem uma alternativa poderosa e flexível à herança tradicional de classes que você conhece do Python. Vamos desmistificar esse conceito fundamental com muitas comparações e exemplos práticos!
No mundo orientado a objetos do Python, a herança é o mecanismo principal para reutilização de código. Você cria uma classe base com comportamentos e propriedades comuns, e depois cria classes derivadas que herdam essas características:
# Exemplo de herança em Python
class Animal:
def fazer_som(self):
raise NotImplementedError("Método abstrato")
def mover(self):
print("Movendo-se...")
class Cachorro(Animal):
def fazer_som(self):
return "Au au!"
class Gato(Animal):
def fazer_som(self):
return "Miau!"
Em Rust, a abordagem é diferente. Em vez de herança, Rust utiliza composição através de traits para compartilhar comportamento entre tipos. Traits definem um contrato de comportamento que tipos podem implementar, sem herdar propriedades ou estado.
Rust prioriza composição sobre herança - um princípio de design que também é recomendado em Python, mas que Rust torna explícito na linguagem.
Traits em Rust são similares a interfaces em outras linguagens de programação, mas com superpoderes! Uma trait define um conjunto de métodos que um tipo deve implementar.
Pense nas traits como contratos de comportamento. Elas dizem: “Se você quiser ser considerado um X, você precisa ser capaz de fazer Y”.
Vamos começar com um exemplo simples:
// Definindo uma trait
trait FazSom {
fn fazer_som(&self) -> String;
}
// Implementando a trait para um tipo específico
struct Cachorro {
nome: String,
}
impl FazSom for Cachorro {
fn fazer_som(&self) -> String {
format!("{} faz: Au au!", self.nome)
}
}
// Outra implementação
struct Gato {
nome: String,
}
impl FazSom for Gato {
fn fazer_som(&self) -> String {
format!("{} faz: Miau!", self.nome)
}
}
No Python, isso seria semelhante a uma classe abstrata com métodos abstratos, mas com uma diferença crucial: em Rust, você pode implementar traits para tipos que você não definiu (desde que a trait ou o tipo estejam no seu crate).
Uma das características mais poderosas das traits são as implementações padrão:
trait FazSom {
fn fazer_som(&self) -> String;
// Método com implementação padrão
fn apresentar(&self) -> String {
format!("Eu sou um animal que faz: {}", self.fazer_som())
}
}
// Agora só precisamos implementar fazer_som()
impl FazSom for Cachorro {
fn fazer_som(&self) -> String {
"Au au!".to_string()
}
}
// Podemos sobrescrever o padrão se quisermos
impl FazSom for Gato {
fn fazer_som(&self) -> String {
"Miau!".to_string()
}
fn apresentar(&self) -> String {
format!("Sou um gato elegante que diz: {}", self.fazer_som())
}
}
Isso é semelhante aos métodos com implementação padrão em classes base abstratas do Python, mas com muito mais flexibilidade.
Agora vamos ver um exemplo mais prático e completo. Vamos criar um sistema simples de formas geométricas com traits:
# Exemplo em Python com herança
class Forma:
def area(self):
raise NotImplementedError
def perimetro(self):
raise NotImplementedError
class Retangulo(Forma):
def __init__(self, largura, altura):
self.largura = largura
self.altura = altura
def area(self):
return self.largura * self.altura
def perimetro(self):
return 2 * (self.largura + self.altura)
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
def area(self):
return 3.14159 * self.raio ** 2
def perimetro(self):
return 2 * 3.14159 * self.raio
Agora veja como ficaria em Rust com traits:
// Definindo a trait FormaGeometrica
trait FormaGeometrica {
fn area(&self) -> f64;
fn perimetro(&self) -> f64;
// Método com implementação padrão que usa os outros métodos da trait
fn descricao(&self) -> String {
format!("Área: {:.2}, Perímetro: {:.2}", self.area(), self.perimetro())
}
}
// Definindo structs para nossas formas
struct Retangulo {
largura: f64,
altura: f64,
}
// Implementando a trait para Retangulo
impl FormaGeometrica for Retangulo {
fn area(&self) -> f64 {
self.largura * self.altura
}
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
}
struct Circulo {
raio: f64,
}
impl FormaGeometrica for Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * self.raio.powi(2)
}
fn perimetro(&self) -> f64 {
2.0 * std::f64::consts::PI * self.raio
}
// Sobrescrevendo o método padrão chamando outros métodos da trait
fn descricao(&self) -> String {
format!(
"Círculo com raio {:.2}: Área: {:.2}, Perímetro: {:.2}",
self.raio,
self.area(),
self.perimetro()
)
}
}
// Função genérica que aceita qualquer tipo que implemente FormaGeometrica
fn imprimir_info<T: FormaGeometrica>(forma: &T) {
println!("{}", forma.descricao());
}
fn main() {
let retangulo = Retangulo { largura: 5.0, altura: 3.0 };
let circulo = Circulo { raio: 2.5 };
imprimir_info(&retangulo);
imprimir_info(&circulo);
}
A diferença fundamental aqui é que em Rust, a trait é um contrato separado da definição do tipo, enquanto em Python o comportamento é definido junto com o tipo através da herança.
⚠️ Importante: Ao sobrescrever um método padrão de uma trait, você deve usar os outros métodos da própria trait (
self.area(),self.perimetro()). Não existe equivalente asuper()do Python — não há hierarquia de herança de estado em Rust.
Uma das características mais interessantes das traits é a capacidade de criar trait objects para polimorfismo em tempo de execução.
Vamos ver primeiro como o polimorfismo funciona em Python:
# Polimorfismo em Python (dinâmico por natureza)
formas = [Retangulo(5, 3), Circulo(2.5)]
for forma in formas:
print(f"Área: {forma.area()}")
print(f"Perímetro: {forma.perimetro()}")
Em Rust, precisamos ser explícitos sobre quando queremos despacho dinâmico usando trait objects:
fn main() {
// Declaramos as variáveis com seus tipos concretos primeiro
let retangulo = Retangulo { largura: 5.0, altura: 3.0 };
let circulo = Circulo { raio: 2.5 };
// Criando um vetor de trait objects: cada elemento é uma referência
// dinâmica a qualquer tipo que implemente FormaGeometrica
let formas: Vec<&dyn FormaGeometrica> = vec![&retangulo, &circulo];
for forma in formas {
println!("{}", forma.descricao());
}
}
dyn?A palavra-chave dyn indica despacho dinâmico (dynamic dispatch). Quando você usa &dyn MinhaTraint, o Rust não sabe em tempo de compilação qual tipo concreto está por trás da referência — ele descobre isso em tempo de execução usando uma vtable (tabela de ponteiros de funções), exatamente como Python faz com todos os seus objetos.
Sem dyn, o Rust precisaria conhecer o tipo exato em tempo de compilação (o que é o caso dos generics, como veremos a seguir).
É importante entender as diferenças entre as três formas principais de usar traits:
// 1. impl Trait para um tipo específico
impl FormaGeometrica for Retangulo { /* ... */ }
// 2. Trait bound em generics (despacho estático — mais rápido)
// O compilador gera uma versão específica para cada tipo em compile-time
fn imprimir<T: FormaGeometrica>(forma: &T) {
println!("{}", forma.descricao());
}
// 3. Trait object com dyn (despacho dinâmico — mais flexível)
// O tipo concreto é resolvido em runtime via vtable
fn imprimir_dinamico(forma: &dyn FormaGeometrica) {
println!("{}", forma.descricao());
}
| Característica | Generics (<T: Trait>) |
Trait Objects (dyn Trait) |
|---|---|---|
| Quando o tipo é resolvido | Compile-time | Runtime |
| Performance | Mais rápida (zero-cost) | Overhead de vtable |
| Coleções heterogêneas | ❌ Não suporta | ✅ Suporta |
| Tamanho em memória | Conhecido em compile-time | Requer ponteiro (Box, &) |
| Caso de uso ideal | Tipos conhecidos, performance crítica | Plugins, coleções polimórficas |
// Generics: melhor performance — tipo resolvido em compile-time
fn processar_generico<T: FormaGeometrica>(forma: &T) {
println!("{}", forma.descricao());
}
// Trait objects: mais flexível — permite coleções heterogêneas
fn processar_dinamico(forma: &dyn FormaGeometrica) {
println!("{}", forma.descricao());
}
// Com Box<dyn Trait>, você pode armazenar valores owned de tipos diferentes
fn criar_formas() -> Vec<Box<dyn FormaGeometrica>> {
vec![
Box::new(Retangulo { largura: 5.0, altura: 3.0 }),
Box::new(Circulo { raio: 2.5 }),
]
}
Traits podem declarar tipos associados (associated types), que permitem que cada implementação defina seus próprios tipos internos. Isso é muito usado em traits da biblioteca padrão como Iterator.
trait Repository {
type Item; // tipo associado — cada implementação define o seu
fn get(&self, id: i32) -> Option<Self::Item>;
fn save(&mut self, item: Self::Item) -> Result<(), String>;
}
struct Usuario {
id: i32,
nome: String,
}
struct UsuarioRepository {
usuarios: Vec<Usuario>,
}
impl Repository for UsuarioRepository {
type Item = Usuario; // aqui definimos o tipo concreto
fn get(&self, id: i32) -> Option<Self::Item> {
self.usuarios.iter()
.find(|u| u.id == id)
.map(|u| Usuario { id: u.id, nome: u.nome.clone() })
}
fn save(&mut self, item: Self::Item) -> Result<(), String> {
self.usuarios.push(item);
Ok(())
}
}
Em Python, você simularia isso com generics de tipo (Generic[T]) ou type hints. Em Rust, os tipos associados tornam a intenção explícita e são verificados pelo compilador.
Traits também podem definir funções associadas — equivalente aos métodos de classe (@classmethod) ou métodos estáticos do Python. A diferença é que essas funções não recebem &self:
trait Fabrica {
fn novo() -> Self; // sem &self — é uma função associada
}
struct Ponto {
x: f64,
y: f64,
}
impl Fabrica for Ponto {
fn novo() -> Self {
Ponto { x: 0.0, y: 0.0 }
}
}
// Equivalente Python:
// class Ponto:
// @classmethod
// def novo(cls):
// return cls(x=0.0, y=0.0)
fn main() {
let p = Ponto::novo(); // chamada via nome do tipo, sem instância
}
Um recurso poderoso e exclusivo do Rust são as blanket implementations: implementar uma trait para todos os tipos que já implementam outra trait. Isso é amplamente usado na biblioteca padrão:
// Exemplo: implementar uma trait para todos os tipos que implementam FormaGeometrica
trait Exibivel {
fn exibir(&self);
}
// Blanket impl: qualquer tipo que impl FormaGeometrica também ganha Exibivel
impl<T: FormaGeometrica> Exibivel for T {
fn exibir(&self) {
println!("Forma -> {}", self.descricao());
}
}
// Agora Retangulo e Circulo ganham exibir() automaticamente!
fn main() {
let r = Retangulo { largura: 4.0, altura: 2.0 };
r.exibir(); // Forma -> Área: 8.00, Perímetro: 12.00
}
A biblioteca padrão usa blanket implementations extensivamente. Por exemplo, o trait ToString é implementado automaticamente para todo tipo que implementa Display:
// Na stdlib: impl<T: Display> ToString for T { ... }
// Por isso podemos chamar .to_string() em qualquer tipo com Display!
Vamos analisar as diferenças fundamentais entre a abordagem do Python e do Rust:
| Característica | Python (Herança) | Rust (Traits) |
|---|---|---|
| Reutilização de código | Através de herança de classes | Através de implementação de traits |
| Herança múltipla | Suportada (com complexidade) | Não existe, mas traits substituem |
| Composição | Possível, mas não forçada | Padrão da linguagem |
| Contratos explícitos | ABCs (Abstract Base Classes) | Traits |
| Despacho de métodos | Sempre dinâmico | Estático (generics) ou dinâmico (trait objects) |
| Extensibilidade | Pode modificar classes existentes | Pode implementar traits para tipos existentes |
Um dos problemas mais conhecidos da herança múltipla em Python é o problema do diamante:
# Problema do diamante em Python
class A:
def metodo(self):
print("A")
class B(A):
def metodo(self):
print("B")
class C(A):
def metodo(self):
print("C")
class D(B, C):
pass
d = D()
d.metodo() # Qual método é chamado? Depende da MRO!
Em Rust, traits resolvem esse problema elegantemente. Como não há herança de estado, cada trait é um contrato independente. Quando B e C são supertraits de A, o tipo D implementa explicitamente cada uma — sem ambiguidade:
trait A {
fn metodo(&self);
}
trait B: A {} // B requer que A esteja implementada, mas não redefine metodo
trait C: A {} // idem para C
struct D;
impl A for D {
fn metodo(&self) {
println!("A implementado para D");
}
}
impl B for D {} // compila porque A já está implementada para D
impl C for D {} // idem
fn main() {
let d = D;
d.metodo(); // sem ambiguidade: só existe uma implementação de metodo para D
}
Se B e C definirem métodos próprios com o mesmo nome, o Rust força o uso de sintaxe qualificada para desambiguar — tornando a intenção explícita, ao contrário do MRO implícito do Python:
trait B: A {
fn comportamento(&self) { println!("Comportamento de B"); }
}
trait C: A {
fn comportamento(&self) { println!("Comportamento de C"); }
}
struct D;
impl A for D { fn metodo(&self) { println!("A"); } }
impl B for D {}
impl C for D {}
fn main() {
let d = D;
B::comportamento(&d); // explícito: chama B::comportamento
C::comportamento(&d); // explícito: chama C::comportamento
}
Rust possui um conjunto rico de traits na stdlib que você deve conhecer e implementar para seus tipos:
| Trait | Finalidade | Equivalente Python |
|---|---|---|
Display / Debug |
Formatação de strings | __str__ / __repr__ |
Clone / Copy |
Clonar / copiar valores | __copy__ / copy.deepcopy |
From / Into |
Conversões entre tipos | __init__ com outro tipo |
Iterator |
Iteração com next() |
__iter__ / __next__ |
Drop |
Cleanup ao sair de escopo | __del__ |
PartialEq / Eq |
Comparação de igualdade | __eq__ |
PartialOrd / Ord |
Comparação de ordem | __lt__, __gt__ |
use std::fmt;
struct Ponto {
x: f64,
y: f64,
}
// Display: como o tipo aparece para o usuário
impl fmt::Display for Ponto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({:.2}, {:.2})", self.x, self.y)
}
}
// From: conversão idiomática entre tipos
impl From<(f64, f64)> for Ponto {
fn from(tupla: (f64, f64)) -> Self {
Ponto { x: tupla.0, y: tupla.1 }
}
}
fn main() {
let p = Ponto::from((3.0, 4.0));
println!("{}", p); // (3.00, 4.00)
// Into é gerado automaticamente quando From é implementado
let p2: Ponto = (1.0, 2.0).into();
println!("{}", p2); // (1.00, 2.00)
}
Vamos destacar alguns erros comuns que programadores Python cometem ao começar com traits em Rust:
Tentar imitar herança hierárquica: Em Rust, pense em composição, não em hierarquia de herança.
Usar dyn quando generics bastam: Prefira generics quando os tipos são conhecidos em compile-time — a performance é melhor (zero-cost abstraction).
Subutilizar enums: Muitas vezes, um enum é uma solução melhor que trait objects quando você sabe todos os tipos possíveis antecipadamente.
// Às vezes é melhor usar enum que trait objects
enum Forma {
Retangulo(Retangulo),
Circulo(Circulo),
}
impl FormaGeometrica for Forma {
fn area(&self) -> f64 {
match self {
Forma::Retangulo(r) => r.area(),
Forma::Circulo(c) => c.area(),
}
}
fn perimetro(&self) -> f64 {
match self {
Forma::Retangulo(r) => r.perimetro(),
Forma::Circulo(c) => c.perimetro(),
}
}
}
dyn: Quando usar trait objects, não esqueça da palavra-chave dyn:
// Errado (Rust moderno exige dyn): Vec<&FormaGeometrica>
// Correto: Vec<&dyn FormaGeometrica>
Debug, Clone, Copy, etc). Implemente-as para seus tipos!Vamos criar um sistema de notificação que pode enviar mensagens por diferentes canais (email, SMS, push):
# Versão Python com herança
class Notificador:
def enviar(self, mensagem: str) -> bool:
raise NotImplementedError
class NotificadorEmail(Notificador):
def __init__(self, email: str):
self.email = email
def enviar(self, mensagem: str) -> bool:
print(f"Enviando email para {self.email}: {mensagem}")
return True
class NotificadorSMS(Notificador):
def __init__(self, telefone: str):
self.telefone = telefone
def enviar(self, mensagem: str) -> bool:
print(f"Enviando SMS para {self.telefone}: {mensagem}")
return True
# Uso
notificadores = [NotificadorEmail("user@example.com"), NotificadorSMS("+551199999999")]
for notificador in notificadores:
notificador.enviar("Olá!")
Agora a versão Rust:
// Versão Rust com traits
trait Notificador {
fn enviar(&self, mensagem: &str) -> bool;
}
struct NotificadorEmail {
email: String,
}
impl Notificador for NotificadorEmail {
fn enviar(&self, mensagem: &str) -> bool {
println!("Enviando email para {}: {}", self.email, mensagem);
true
}
}
struct NotificadorSMS {
telefone: String,
}
impl Notificador for NotificadorSMS {
fn enviar(&self, mensagem: &str) -> bool {
println!("Enviando SMS para {}: {}", self.telefone, mensagem);
true
}
}
struct NotificadorPush {
device_id: String,
}
impl Notificador for NotificadorPush {
fn enviar(&self, mensagem: &str) -> bool {
println!("Enviando notificação push para {}: {}", self.device_id, mensagem);
true
}
}
fn main() {
// Box<dyn Trait> permite armazenar tipos owned diferentes no mesmo vetor
let notificadores: Vec<Box<dyn Notificador>> = vec![
Box::new(NotificadorEmail { email: "user@example.com".to_string() }),
Box::new(NotificadorSMS { telefone: "+551199999999".to_string() }),
Box::new(NotificadorPush { device_id: "device123".to_string() }),
];
for notificador in notificadores {
notificador.enviar("Olá!");
}
}
Neste post exploramos as traits em Rust e como elas oferecem uma alternativa poderosa à herança tradicional do Python:
dyn e despacho dinâmico: dyn Trait sinaliza que o tipo é resolvido em runtime via vtable — use quando precisar de coleções heterogêneas&self, equivalentes a @classmethod do PythonDisplay, From/Into, Iterator, Drop e outras devem ser implementadas para seus tiposTraits são um dos conceitos mais poderosos do Rust, oferecendo flexibilidade e segurança ao mesmo tempo. Elas representam uma maneira moderna de pensar sobre polimorfismo e reutilização de código, diferente mas complementar à herança tradicional do Python.
💡 Quer se aprofundar ainda mais em Rust?
Adquira o livro ‘Desbravando Rust’ para uma jornada completa desde os fundamentos até tópicos avançados!
📖 Disponível em: desbravandorust.com.br
Nos próximos posts, continuaremos explorando conceitos avançados de Rust. Deixe nos comentários quais tópicos você gostaria de ver explicados!