Rust

Traits — Definindo Comportamento Compartilhado Já leu

12 min de leitura

Traits — Definindo Comportamento Compartilhado
  Nos artigos anteriores criamos structs e enums para modelar dados. Mas programas reais precisam de mais do que dados — precisam de comportame

 

Nos artigos anteriores 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.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Coleções — Vec, HashMap e HashSet na Prática
Coleções — Vec, HashMap e HashSet na Prática

&nbsp; At&eacute; agora trabalhamos com dados de tamanho fixo &mdash; arrays...

Controle de Fluxo — if, loop, while e for como você nunca viu antes
Controle de Fluxo — if, loop, while e for como você nunca viu antes

No artigo anterior, aprendemos que Rust distingue statements de expressions,...

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...