Traits e Generics em Rust: Tutorial | Rust Brasil

Tutorial de traits e generics em Rust: trait bounds, impl Trait, where clauses e polimorfismo. Guia completo em português.

Introdução

Traits e generics são dois dos recursos mais poderosos do Rust. Juntos, eles permitem escrever código altamente reutilizável, seguro em tempo de compilação e com abstrações de custo zero. Neste tutorial, vamos explorar como definir e implementar traits, usar generics para criar código flexível e combinar ambos para construir sistemas robustos.

O Que São Traits?

Uma trait em Rust define um conjunto de comportamentos (métodos) que um tipo pode implementar. É semelhante a interfaces em outras linguagens, mas com recursos adicionais como implementações padrão e tipos associados.

Definindo uma Trait

Vamos começar definindo uma trait simples:

trait Resumo {
    fn resumir(&self) -> String;

    // Método com implementação padrão
    fn preview(&self) -> String {
        format!("{}...", &self.resumir()[..50.min(self.resumir().len())])
    }
}

A trait Resumo exige que qualquer tipo que a implemente forneça o método resumir. O método preview possui uma implementação padrão que pode ser sobrescrita.

Implementando Traits

Agora vamos implementar essa trait para diferentes tipos:

struct Artigo {
    titulo: String,
    autor: String,
    conteudo: String,
}

struct Tweet {
    usuario: String,
    texto: String,
    curtidas: u32,
}

impl Resumo for Artigo {
    fn resumir(&self) -> String {
        format!("{} por {} - {}", self.titulo, self.autor, &self.conteudo[..100.min(self.conteudo.len())])
    }
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("@{}: {} ({} curtidas)", self.usuario, self.texto, self.curtidas)
    }

    // Sobrescrevendo a implementação padrão
    fn preview(&self) -> String {
        format!("@{}: {}...", self.usuario, &self.texto[..30.min(self.texto.len())])
    }
}

Usando a Trait

fn main() {
    let artigo = Artigo {
        titulo: String::from("Rust no Brasil"),
        autor: String::from("Maria"),
        conteudo: String::from("Rust é uma linguagem de programação que tem crescido muito no Brasil nos últimos anos, com comunidades ativas em várias cidades."),
    };

    let tweet = Tweet {
        usuario: String::from("rustacean_br"),
        texto: String::from("Acabei de descobrir traits em Rust!"),
        curtidas: 42,
    };

    println!("{}", artigo.resumir());
    println!("{}", tweet.resumir());
}

Generics: Código Flexível e Reutilizável

Generics permitem que você escreva código que funciona com diferentes tipos sem duplicação. O compilador do Rust realiza monomorfização, gerando código especializado para cada tipo concreto usado.

Funções Genéricas

fn maior<T: PartialOrd>(lista: &[T]) -> &T {
    let mut maior = &lista[0];
    for item in &lista[1..] {
        if item > maior {
            maior = item;
        }
    }
    maior
}

fn main() {
    let numeros = vec![34, 50, 25, 100, 65];
    println!("O maior número é {}", maior(&numeros));

    let caracteres = vec!['a', 'z', 'm', 'b'];
    println!("O maior caractere é {}", maior(&caracteres));
}

Structs Genéricas

struct Ponto<T> {
    x: T,
    y: T,
}

impl<T: std::fmt::Display> Ponto<T> {
    fn exibir(&self) {
        println!("Ponto({}, {})", self.x, self.y);
    }
}

// Implementação específica para f64
impl Ponto<f64> {
    fn distancia_origem(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let ponto_inteiro = Ponto { x: 5, y: 10 };
    ponto_inteiro.exibir();

    let ponto_float = Ponto { x: 3.0_f64, y: 4.0_f64 };
    ponto_float.exibir();
    println!("Distância da origem: {}", ponto_float.distancia_origem());
}

Structs com Múltiplos Tipos Genéricos

struct Par<T, U> {
    primeiro: T,
    segundo: U,
}

impl<T: std::fmt::Debug, U: std::fmt::Debug> Par<T, U> {
    fn new(primeiro: T, segundo: U) -> Self {
        Par { primeiro, segundo }
    }

    fn mostrar(&self) {
        println!("Par: ({:?}, {:?})", self.primeiro, self.segundo);
    }
}

fn main() {
    let par = Par::new("nome", 42);
    par.mostrar(); // Par: ("nome", 42)
}

Trait Bounds: Restringindo Generics

Trait bounds permitem especificar quais comportamentos um tipo genérico deve ter.

Sintaxe com where

Para funções com muitos trait bounds, a cláusula where torna o código mais legível:

use std::fmt::{Display, Debug};

fn imprimir_comparacao<T>(a: &T, b: &T)
where
    T: Display + PartialOrd + Debug,
{
    if a > b {
        println!("{} é maior que {}", a, b);
    } else if a < b {
        println!("{} é menor que {}", a, b);
    } else {
        println!("{} é igual a {}", a, b);
    }
    println!("Debug: {:?} vs {:?}", a, b);
}

fn main() {
    imprimir_comparacao(&10, &20);
    imprimir_comparacao(&"abacaxi", &"banana");
}

Retornando Tipos que Implementam Traits

fn criar_resumivel() -> impl Resumo {
    Artigo {
        titulo: String::from("Tutorial Rust"),
        autor: String::from("Comunidade"),
        conteudo: String::from("Este é um tutorial completo sobre Rust para desenvolvedores brasileiros que querem aprender a linguagem."),
    }
}

Tipos Associados

Tipos associados são uma forma de conectar um tipo placeholder a uma trait, permitindo que a implementação defina o tipo concreto:

trait Iterador {
    type Item;

    fn proximo(&mut self) -> Option<Self::Item>;
}

struct Contador {
    valor: u32,
    maximo: u32,
}

impl Contador {
    fn new(maximo: u32) -> Self {
        Contador { valor: 0, maximo }
    }
}

impl Iterador for Contador {
    type Item = u32;

    fn proximo(&mut self) -> Option<Self::Item> {
        if self.valor < self.maximo {
            self.valor += 1;
            Some(self.valor)
        } else {
            None
        }
    }
}

fn main() {
    let mut contador = Contador::new(5);
    while let Some(valor) = contador.proximo() {
        println!("Contagem: {}", valor);
    }
}

A diferença entre tipos associados e generics é que, com tipos associados, só pode haver uma implementação da trait para cada tipo. Com generics, um tipo poderia implementar a trait múltiplas vezes com diferentes parâmetros de tipo.

Sobrecarga de Operadores

Em Rust, a sobrecarga de operadores é feita implementando traits do módulo std::ops:

use std::ops::{Add, Mul};
use std::fmt;

#[derive(Debug, Clone, Copy)]
struct Vetor2D {
    x: f64,
    y: f64,
}

impl Vetor2D {
    fn new(x: f64, y: f64) -> Self {
        Vetor2D { x, y }
    }

    fn magnitude(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

impl Add for Vetor2D {
    type Output = Self;

    fn add(self, outro: Self) -> Self::Output {
        Vetor2D {
            x: self.x + outro.x,
            y: self.y + outro.y,
        }
    }
}

impl Mul<f64> for Vetor2D {
    type Output = Self;

    fn mul(self, escalar: f64) -> Self::Output {
        Vetor2D {
            x: self.x * escalar,
            y: self.y * escalar,
        }
    }
}

impl fmt::Display for Vetor2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({:.2}, {:.2})", self.x, self.y)
    }
}

fn main() {
    let v1 = Vetor2D::new(3.0, 4.0);
    let v2 = Vetor2D::new(1.0, 2.0);

    let soma = v1 + v2;
    println!("v1 + v2 = {}", soma);
    println!("Magnitude de v1: {:.2}", v1.magnitude());

    let escalado = v1 * 2.5;
    println!("v1 * 2.5 = {}", escalado);
}

Exemplo Prático Completo: Sistema de Notificações

Vamos combinar tudo em um exemplo real:

use std::fmt;

trait Notificavel: fmt::Display {
    type Destino;

    fn enviar(&self, destino: &Self::Destino) -> Result<(), String>;
    fn prioridade(&self) -> u8;

    fn eh_urgente(&self) -> bool {
        self.prioridade() > 7
    }
}

#[derive(Debug)]
struct Email {
    assunto: String,
    corpo: String,
    prioridade: u8,
}

struct EnderecoEmail(String);

impl fmt::Display for Email {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[Email] {}: {}", self.assunto, self.corpo)
    }
}

impl Notificavel for Email {
    type Destino = EnderecoEmail;

    fn enviar(&self, destino: &Self::Destino) -> Result<(), String> {
        println!("Enviando email para {}: {}", destino.0, self.assunto);
        Ok(())
    }

    fn prioridade(&self) -> u8 {
        self.prioridade
    }
}

fn processar_notificacoes<N>(notificacoes: &[N])
where
    N: Notificavel + fmt::Display,
{
    for notif in notificacoes {
        if notif.eh_urgente() {
            println!("URGENTE: {}", notif);
        } else {
            println!("Normal: {}", notif);
        }
    }
}

fn main() {
    let emails = vec![
        Email {
            assunto: String::from("Reunião amanhã"),
            corpo: String::from("Lembrete da reunião de equipe"),
            prioridade: 5,
        },
        Email {
            assunto: String::from("Servidor fora do ar!"),
            corpo: String::from("O servidor de produção caiu"),
            prioridade: 10,
        },
    ];

    processar_notificacoes(&emails);

    let destino = EnderecoEmail(String::from("dev@rustbrasil.org"));
    for email in &emails {
        let _ = email.enviar(&destino);
    }
}

Conclusão

Traits e generics são pilares fundamentais do sistema de tipos do Rust. Com traits, definimos comportamentos compartilhados de forma abstrata. Com generics, escrevemos código que funciona com múltiplos tipos. Combinados, eles oferecem:

  • Reutilização de código sem sacrificar performance
  • Segurança em tempo de compilação com verificação de tipos
  • Abstrações de custo zero graças à monomorfização
  • Flexibilidade para modelar domínios complexos

Pratique criando suas próprias traits e estruturas genéricas. À medida que você se familiarizar com esses conceitos, perceberá que eles são a base de praticamente toda a biblioteca padrão do Rust e do ecossistema de crates.