Traits — Definindo Comportamento Compartilhado

Traits — Definindo Comportamento Compartilhado

Trait descreve comportamento, não estrutura, e é o que Rust oferece no lugar da herança. O texto trata das implementações padrão, das duas sintaxes de parâmetro, dos trait objects que permitem tipos diferentes na mesma coleção, e do que muda quando o reuso passa a ser por composição.
Rust

15 min de leitura

 

Nos artigos anteriores (Tratamento de Erros com Result — Erros como Valores, não Exceções, Coleções — Vec, HashMap e HashSet na Prática, Iteradores e Closures — O Estilo Funcional de Rust) criamos structs e enums para modelar dados. Mas programas reais precisam de mais do que dados — precisam de comportamento. Como garantir que tipos diferentes possam ser usados de forma intercambiável? Como definir contratos que o compilador verifica? Em linguagens orientadas a objetos, a resposta seria herança e interfaces. Em Rust, a resposta é traits.

Traits são o mecanismo central de abstração de Rust. Eles aparecem em todo lugar — nos iteradores que estudamos, no sistema de erros, na formatação com println!. Entender traits é entender como Rust pensa sobre polimorfismo.

O que é um trait

Um trait define um conjunto de métodos que um tipo deve implementar. É um contrato: qualquer tipo que implemente o trait garante que possui aqueles comportamentos.

trait Descricao {
    fn descrever(&self) -> String;
}

struct Carro {
    marca: String,
    ano: u32,
}

struct Bicicleta {
    tipo: String,
    marchas: u8,
}

impl Descricao for Carro {
    fn descrever(&self) -> String {
        format!("{} ({})", self.marca, self.ano)
    }
}

impl Descricao for Bicicleta {
    fn descrever(&self) -> String {
        format!("Bicicleta {} com {} marchas", self.tipo, self.marchas)
    }
}

fn imprimir_descricao(item: &impl Descricao) {
    println!("{}", item.descrever());
}

fn main() {
    let carro = Carro {
        marca: String::from("Toyota"),
        ano: 2022,
    };

    let bike = Bicicleta {
        tipo: String::from("mountain"),
        marchas: 21,
    };

    imprimir_descricao(&carro);
    imprimir_descricao(&bike);
}

Saída:

Toyota (2022)
Bicicleta mountain com 21 marchas

&impl Descricao é a sintaxe moderna para dizer: "aceito qualquer tipo que implemente o trait Descricao". O compilador resolve qual implementação usar em tempo de compilação — sem custo em execução.

Métodos com implementação padrão

Traits podem fornecer implementações padrão para seus métodos. Tipos que implementam o trait podem usar o padrão ou sobrescrevê-lo:

trait Saudacao {
    fn nome(&self) -> &str;

    // Implementação padrão — usa nome()
    fn saudar(&self) -> String {
        format!("Olá, {}!", self.nome())
    }

    // Implementação padrão mais elaborada
    fn saudar_formal(&self) -> String {
        format!("Bom dia, Sr./Sra. {}. Como posso ajudá-lo?", self.nome())
    }
}

struct Cliente {
    nome: String,
}

struct Funcionario {
    nome: String,
    cargo: String,
}

impl Saudacao for Cliente {
    fn nome(&self) -> &str {
        &self.nome
    }
    // usa saudar() e saudar_formal() padrão
}

impl Saudacao for Funcionario {
    fn nome(&self) -> &str {
        &self.nome
    }

    // sobrescreve saudar()
    fn saudar(&self) -> String {
        format!("Olá! Sou {}, {}.", self.nome, self.cargo)
    }
    // mantém saudar_formal() padrão
}

fn main() {
    let cliente = Cliente { nome: String::from("Ana Silva") };
    let func = Funcionario {
        nome: String::from("Carlos"),
        cargo: String::from("Gerente"),
    };

    println!("{}", cliente.saudar());
    println!("{}", cliente.saudar_formal());
    println!("{}", func.saudar());
    println!("{}", func.saudar_formal());
}

Saída:

Olá, Ana Silva!
Bom dia, Sr./Sra. Ana Silva. Como posso ajudá-lo?
Olá! Sou Carlos, Gerente.
Bom dia, Sr./Sra. Carlos. Como posso ajudá-lo?

Métodos padrão podem chamar outros métodos do mesmo trait — mesmo que esses outros métodos não tenham implementação padrão. Isso cria hierarquias de comportamento expressivas com código mínimo.

Traits como parâmetros — duas sintaxes

Há duas formas equivalentes de aceitar traits como parâmetros de função:

// Sintaxe impl Trait — concisa, recomendada para casos simples
fn resumir(item: &impl Resumivel) -> String {
    item.resumo()
}

// Sintaxe de bound genérico — mais explícita, necessária em casos complexos
fn resumir<T: Resumivel>(item: &T) -> String {
    item.resumo()
}

Ambas são equivalentes para um único parâmetro. A segunda forma se torna necessária quando você precisa que dois parâmetros sejam do mesmo tipo:

// Garante que item1 e item2 são do mesmo tipo T
fn comparar<T: Resumivel>(item1: &T, item2: &T) {
    println!("1: {}", item1.resumo());
    println!("2: {}", item2.resumo());
}

Com impl Trait, os dois parâmetros poderiam ser de tipos diferentes — desde que ambos implementem o trait.

Múltiplos bounds com +

Você pode exigir que um tipo implemente múltiplos traits ao mesmo tempo com +:

use std::fmt;

traitArea {
    fn area(&self) -> f64;
}

// T deve implementar Area E fmt::Display
fn exibir_area<T: Area + fmt::Display>(forma: &T) {
    println!("Forma: {forma}");
    println!("Área: {:.2}", forma.area());
}

Para muitos bounds, a cláusula where torna o código mais legível:

fn processar<T, U>(t: &T, u: &U) -> String
where
    T: Descricao + fmt::Debug,
    U: Descricao + Clone,
{
    format!("{} | {}", t.descrever(), u.descrever())
}

A cláusula where separa os bounds da assinatura da função, tornando ambos mais fáceis de ler quando a lista de restrições cresce.

Retornando traits com impl Trait

Funções também podem retornar tipos que implementam um trait sem nomear o tipo concreto:

trait Transformar {
    fn transformar(&self) -> String;
}

struct Maiusculo(String);
struct Invertido(String);

impl Transformar for Maiusculo {
    fn transformar(&self) -> String {
        self.0.to_uppercase()
    }
}

impl Transformar for Invertido {
    fn transformar(&self) -> String {
        self.0.chars().rev().collect()
    }
}

fn criar_transformador(inverter: bool, texto: &str) -> impl Transformar {
    if inverter {
        Invertido(texto.to_string())
    } else {
        Maiusculo(texto.to_string())
    }
}

Há uma limitação importante: quando você retorna impl Trait, todos os caminhos de retorno devem retornar o mesmo tipo concreto. O exemplo acima não compila porque retorna Invertido em um ramo e Maiusculo em outro. Para retornar tipos diferentes dinamicamente, precisamos de trait objects — que veremos a seguir.

Trait Objects — polimorfismo dinâmico

Às vezes você precisa de uma coleção com tipos diferentes que compartilham um trait, ou uma função que retorna tipos diferentes em tempo de execução. Para isso, Rust oferece trait objects com dyn:

trait Animal {
    fn som(&self) -> &str;
    fn nome(&self) -> &str;
}

struct Cachorro;
struct Gato;
struct Passaro;

impl Animal for Cachorro {
    fn som(&self) -> &str { "Au au!" }
    fn nome(&self) -> &str { "Cachorro" }
}

impl Animal for Gato {
    fn som(&self) -> &str { "Miau!" }
    fn nome(&self) -> &str { "Gato" }
}

impl Animal for Passaro {
    fn som(&self) -> &str { "Piu piu!" }
    fn nome(&self) -> &str { "Pássaro" }
}

fn main() {
    // Vec de trait objects — tipos diferentes numa mesma coleção
    let animais: Vec<Box<dyn Animal>> = vec![
        Box::new(Cachorro),
        Box::new(Gato),
        Box::new(Passaro),
        Box::new(Cachorro),
    ];

    for animal in &animais {
        println!("{}: {}", animal.nome(), animal.som());
    }
}

Saída:

Cachorro: Au au!
Gato: Miau!
Pássaro: Piu piu!
Cachorro: Au au!

Box<dyn Animal> é um trait object — um ponteiro para um valor de tipo desconhecido em tempo de compilação, que implementa Animal. O Box é necessário porque o compilador não sabe o tamanho do tipo concreto em tempo de compilação.

A diferença fundamental entre impl Trait e dyn Trait:

  • impl Traitdispatch estático: o compilador gera código específico para cada tipo. Mais rápido, sem custo em execução.
  • dyn Traitdispatch dinâmico: o tipo é resolvido em execução via vtable. Pequeno custo, mas permite coleções heterogêneas e retornos polimórficos.

Traits importantes da biblioteca padrão

A biblioteca padrão de Rust é construída sobre traits. Conhecer os principais é essencial para escrever código idiomático.

Display e Debug

use std::fmt;

struct Ponto {
    x: f64,
    y: f64,
}

// Display — para exibição ao usuário com {}
impl fmt::Display for Ponto {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// Debug — para inspeção com {:?}, pode ser derivado
impl fmt::Debug for Ponto {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Ponto {{ x: {}, y: {} }}", self.x, self.y)
    }
}

fn main() {
    let p = Ponto { x: 3.0, y: -1.5 };
    println!("{p}");   // (3, -1.5)
    println!("{p:?}"); // Ponto { x: 3, y: -1.5 }
}

PartialEq, Eq, PartialOrd, Ord

#[derive(Debug, PartialEq, PartialOrd)]
struct Temperatura {
    celsius: f64,
}

impl Temperatura {
    fn nova(celsius: f64) -> Self {
        Temperatura { celsius }
    }

    fn em_fahrenheit(&self) -> f64 {
        self.celsius * 1.8 + 32.0
    }
}

fn main() {
    let t1 = Temperatura::nova(20.0);
    let t2 = Temperatura::nova(35.0);
    let t3 = Temperatura::nova(20.0);

    println!("t1 == t3: {}", t1 == t3);  // true
    println!("t1 < t2: {}",  t1 < t2);   // true
    println!("t2 > t1: {}",  t2 > t1);   // true

    let mut temps = vec![
        Temperatura::nova(35.0),
        Temperatura::nova(20.0),
        Temperatura::nova(28.0),
    ];
    temps.sort_by(|a, b| a.partial_cmp(b).unwrap());
    println!("{:?}", temps);
}

Clone e Copy

#[derive(Debug, Clone)]
struct Configuracao {
    host: String,
    porta: u16,
    timeout: u32,
}

fn main() {
    let config_original = Configuracao {
        host: String::from("localhost"),
        porta: 8080,
        timeout: 30,
    };

    // Clone cria uma cópia independente
    let mut config_teste = config_original.clone();
    config_teste.porta = 9090;
    config_teste.timeout = 5;

    println!("Original: {:?}", config_original);
    println!("Teste: {:?}", config_teste);
}

Um programa completo: sistema de formas geométricas

use std::fmt;

trait Forma {
    fn area(&self) -> f64;
    fn perimetro(&self) -> f64;
    fn nome(&self) -> &str;

    fn descricao(&self) -> String {
        format!(
            "{}: área={:.2}, perímetro={:.2}",
            self.nome(),
            self.area(),
            self.perimetro()
        )
    }
}

struct Circulo {
    raio: f64,
}

struct Retangulo {
    largura: f64,
    altura: f64,
}

struct TrianguloRetangulo {
    cateto_a: f64,
    cateto_b: f64,
}

impl Forma for Circulo {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.raio * self.raio
    }
    fn perimetro(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.raio
    }
    fn nome(&self) -> &str { "Círculo" }
}

impl Forma for Retangulo {
    fn area(&self) -> f64 { self.largura * self.altura }
    fn perimetro(&self) -> f64 { 2.0 * (self.largura + self.altura) }
    fn nome(&self) -> &str { "Retângulo" }
}

impl TrianguloRetangulo {
    fn hipotenusa(&self) -> f64 {
        (self.cateto_a.powi(2) + self.cateto_b.powi(2)).sqrt()
    }
}

impl Forma for TrianguloRetangulo {
    fn area(&self) -> f64 { self.cateto_a * self.cateto_b / 2.0 }
    fn perimetro(&self) -> f64 {
        self.cateto_a + self.cateto_b + self.hipotenusa()
    }
    fn nome(&self) -> &str { "Triângulo Retângulo" }
}

impl fmt::Display for dyn Forma {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.descricao())
    }
}

fn maior_area(formas: &[Box<dyn Forma>]) -> Option<&dyn Forma> {
    formas.iter()
        .max_by(|a, b| a.area().partial_cmp(&b.area()).unwrap())
        .map(|f| f.as_ref())
}

fn area_total(formas: &[Box<dyn Forma>]) -> f64 {
    formas.iter().map(|f| f.area()).sum()
}

fn main() {
    let formas: Vec<Box<dyn Forma>> = vec![
        Box::new(Circulo { raio: 5.0 }),
        Box::new(Retangulo { largura: 8.0, altura: 4.0 }),
        Box::new(TrianguloRetangulo { cateto_a: 3.0, cateto_b: 4.0 }),
        Box::new(Circulo { raio: 2.5 }),
        Box::new(Retangulo { largura: 6.0, altura: 6.0 }),
    ];

    println!("── Formas Geométricas ──────────────────");
    for forma in &formas {
        println!("{}", forma.descricao());
    }

    println!("\nÁrea total: {:.2}", area_total(&formas));

    if let Some(maior) = maior_area(&formas) {
        println!("Maior área: {}", maior.descricao());
    }

    println!("\nApenas círculos:");
    for forma in formas.iter().filter(|f| f.nome() == "Círculo") {
        println!("  {}", forma.descricao());
    }
}

Saída:

── Formas Geométricas ──────────────────
Círculo: área=78.54, perímetro=31.42
Retângulo: área=32.00, perímetro=24.00
Triângulo Retângulo: área=6.00, perímetro=12.00
Círculo: área=19.63, perímetro=15.71
Retângulo: área=36.00, perímetro=24.00

Área total: 172.17
Maior área: Círculo: área=78.54, perímetro=31.42

Apenas círculos:
  Círculo: área=78.54, perímetro=31.42
  Círculo: área=19.63, perímetro=15.71

Traits vs Herança — uma mudança de mentalidade

Quem vem de Java ou Python frequentemente pergunta: onde está a herança? Em Rust, herança de implementação simplesmente não existe. E isso é uma escolha deliberada.

Herança cria acoplamento rígido entre tipos — mudanças em classes pai afetam silenciosamente todos os filhos. Traits favorecem composição: um tipo implementa os traits que fazem sentido para ele, sem precisar de uma hierarquia de classes.

Um tipo pode implementar dezenas de traits independentes. Isso é muito mais flexível do que uma cadeia de herança onde você precisa herdar tudo ou nada de um pai.

Com o tempo, você vai perceber que traits são mais expressivos do que herança — e muito menos propensos a criar os problemas clássicos de hierarquias profundas e frágeis.

Exercícios

Exercício 1

Defina um trait Resumir com um método que tenha implementação padrão e outro que não tenha, e implemente-o para duas structs.

Ver resposta

✓ Resposta:

trait Resumir {
    fn autor(&self) -> String;

    fn resumo(&self) -> String {
        format!("(leia mais de {}...)", self.autor())
    }
}

struct Post { usuario: String }
struct Artigo { assinatura: String }

impl Resumir for Post {
    fn autor(&self) -> String { format!("@{}", self.usuario) }
}

impl Resumir for Artigo {
    fn autor(&self) -> String { self.assinatura.clone() }
    fn resumo(&self) -> String { format!("Por {}", self.assinatura) }
}

Post herda o resumo padrão; Artigo o sobrescreve. O método padrão pode chamar outros do próprio trait, mesmo sem saber como serão implementados.

Exercício 2

Explique a diferença entre impl Trait em parâmetro e dyn Trait em trait object, dizendo quando cada um é necessário.

Ver resposta

✓ Resposta: fn f(x: &impl Resumir) é açúcar para um genérico com bound: o compilador gera uma versão da função para cada tipo concreto usado — despacho estático, sem custo em execução. &dyn Resumir é um trait object: o tipo concreto só é conhecido em execução, e a chamada passa por uma tabela de métodos (vtable). O dyn é necessário quando tipos diferentes precisam conviver na mesma coleção, como Vec<Box<dyn Resumir>> — impossível com genérico, que exige um tipo só. A troca é desempenho por flexibilidade.

Exercício 3

Por que o código abaixo não compila, e qual é a correção?

let formas: Vec<dyn Forma> = vec![circulo, quadrado];
Ver resposta

✓ Resposta: dyn Forma não tem tamanho conhecido em tempo de compilação — cada implementação ocupa um espaço diferente —, e o Vec precisa saber quanto reservar por elemento. O erro é the size for values of type (dyn Forma) cannot be known at compilation time. A correção é guardar atrás de um ponteiro, que tem tamanho fixo:

let formas: Vec<Box<dyn Forma>> = vec![Box::new(circulo), Box::new(quadrado)];

Cada elemento passa a ser um ponteiro para o heap, mais o ponteiro para a vtable.

Exercício 4

Explique o que #[derive(Debug, Clone, PartialEq)] faz e por que nem todo trait pode ser derivado.

Ver resposta

✓ Resposta: O derive gera a implementação automaticamente a partir da estrutura do tipo: Debug imprime os campos, Clone clona campo a campo, PartialEq compara campo a campo. Só funciona quando todos os campos também implementam o trait — uma struct com um campo sem Clone não pode derivar Clone. Traits com lógica própria, como Display, não são deriváveis porque não há resposta óbvia: a forma de exibir um tipo para o usuário é uma decisão de projeto, não algo dedutível dos campos.

Exercício 5

Um colega diz que trait é a forma de fazer herança em Rust. Corrija, apontando a diferença central.

Ver resposta

✓ Resposta: Trait descreve comportamento, não estrutura, e não existe herança de dados em Rust: um trait nunca traz campos consigo, e um tipo não herda o estado de outro. O reuso de dados se faz por composição — guardar uma struct dentro da outra. Um trait pode exigir outro (trait Cachorro: Animal), mas isso é requisito de implementação, não herança de estado. A consequência prática é que não existe a hierarquia frágil de classes: um tipo implementa quantos traits precisar, e traits novos podem ser implementados para tipos já existentes sem tocar neles.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Programação de Redes — TCP, UDP e Sockets
Programação de Redes — TCP, UDP e Sockets

TCP entrega um fluxo de bytes, não mensagens, e tratar o retorno de read como…

Rust e Blockchain — Smart Contracts, Criptografia Aplicada e Web3
Rust e Blockchain — Smart Contracts, Criptografia Aplicada e Web3

Blockchain é menos sobre moeda e mais sobre tornar a alteração do passado cara…

FFI — Integrando Rust com C e Outras Linguagens
FFI — Integrando Rust com C e Outras Linguagens

Atravessar a fronteira com C exige acertar duas coisas ao mesmo tempo: a…