Desbravando Rust

Desmistificando Traits em Rust: Herança para Pythonistas

👋 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!

Introdução: Herança vs Composição

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.

🧩 O que são Traits e como funcionam

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”.

Sintaxe básica de Traits

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).

Traits com implementações padrão

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.

🔧 Implementando Traits para tipos customizados

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 a super() do Python — não há hierarquia de herança de estado em Rust.

🎭 Trait Objects e Despacho Dinâmico

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());
    }
}

O que é 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).

As três formas de usar traits em Rust

É 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());
}

Trait Objects vs Generics

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 }),
    ]
}

🔑 Tipos Associados em Traits

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.

⚙️ Métodos Associados (Funções Estáticas)

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
}

🌐 Implementações Genéricas (Blanket Implementations)

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!

🔄 Comparação com Python: Herança vs Traits

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

Herança múltipla vs Traits

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
}

📚 Traits Importantes da Biblioteca Padrão

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)
}

⚠️ Erros Comuns de Pythonistas em Rust

Vamos destacar alguns erros comuns que programadores Python cometem ao começar com traits em Rust:

  1. Tentar imitar herança hierárquica: Em Rust, pense em composição, não em hierarquia de herança.

  2. Usar dyn quando generics bastam: Prefira generics quando os tipos são conhecidos em compile-time — a performance é melhor (zero-cost abstraction).

  3. 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(),
        }
    }
}
  1. Esquecer o dyn: Quando usar trait objects, não esqueça da palavra-chave dyn:
    // Errado (Rust moderno exige dyn):  Vec<&FormaGeometrica>
    // Correto:                          Vec<&dyn FormaGeometrica>
    
  2. Não usar traits padrão: Rust tem muitas traits úteis na biblioteca padrão (Debug, Clone, Copy, etc). Implemente-as para seus tipos!

🏗️ Exemplo Prático Completo: Sistema de Notificação

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á!");
    }
}

📚 O que aprendemos

Neste post exploramos as traits em Rust e como elas oferecem uma alternativa poderosa à herança tradicional do Python:

Traits 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!