Trait bounds e where clauses

1. Fundamentos das Trait Bounds

Trait bounds são restrições que aplicamos a tipos genéricos em Rust para garantir que eles implementem determinados comportamentos. Sem eles, o compilador não saberia quais operações são permitidas em um tipo genérico. Por exemplo, se você tenta imprimir um valor genérico com println!, o compilador precisa saber que o tipo implementa Display.

A sintaxe básica é direta:

use std::fmt::Display;

fn imprimir<T: Display>(item: T) {
    println!("{}", item);
}

fn main() {
    imprimir(42);
    imprimir("Olá");
}

Aqui, T: Display é um trait bound que garante que apenas tipos que implementam Display podem ser passados para a função.

Traits comuns da biblioteca padrão como Clone, Debug e PartialEq são frequentemente usados como bounds:

fn clonar_e_mostrar<T: Clone + Debug>(item: &T) {
    let copia = item.clone();
    println!("Original: {:?}, Cópia: {:?}", item, copia);
}

#[derive(Debug, Clone)]
struct Ponto(i32, i32);

fn main() {
    let p = Ponto(10, 20);
    clonar_e_mostrar(&p);
}

2. Múltiplas Trait Bounds com +

Quando precisamos de múltiplas restrições, combinamos os traits com o operador +:

use std::fmt::Debug;

fn comparar_e_mostrar<T: Debug + PartialEq>(a: &T, b: &T) {
    println!("{:?} == {:?} é {}", a, b, a == b);
}

fn main() {
    comparar_e_mostrar(&5, &5);
    comparar_e_mostrar(&"abc", &"def");
}

Boas práticas ao usar múltiplos bounds:
- Mantenha os bounds relevantes e necessários
- Evite mais de 3-4 bounds na assinatura direta
- Considere usar where clauses para melhor legibilidade

O uso excessivo de + pode tornar o código confuso:

// Difícil de ler
fn funcao_complexa<T: Clone + Debug + PartialEq + Display + Hash>(t: T) { }

3. Where Clauses: Sintaxe Alternativa e Mais Limpa

A cláusula where permite separar os bounds da assinatura da função, melhorando a legibilidade especialmente em casos complexos:

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

// Sintaxe direta (menos legível com muitos bounds)
fn processar_direto<T: Clone + Debug + Display + Hash>(item: T) {
    println!("Item: {}", item);
}

// Sintaxe com where (mais limpa)
fn processar_where<T>(item: T)
where
    T: Clone + Debug + Display + Hash,
{
    println!("Item: {}", item);
}

As vantagens são mais evidentes com múltiplos parâmetros genéricos:

use std::fmt::Debug;

fn combinar<T, U>(a: T, b: U) -> String
where
    T: Debug + Clone,
    U: Debug + Clone + PartialEq,
{
    format!("{:?} combinado com {:?}", a, b)
}

fn main() {
    println!("{}", combinar(10, "teste"));
}

4. Where Clauses em Structs e Enums

Structs genéricos também podem usar where clauses:

use std::fmt::Display;

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

impl<T, U> Par<T, U>
where
    T: Display,
    U: Clone,
{
    fn novo(primeiro: T, segundo: U) -> Self {
        Par { primeiro, segundo }
    }

    fn mostrar(&self) {
        println!("Primeiro: {}, Segundo (clonado): {:?}", 
                 self.primeiro, self.segundo.clone());
    }
}

fn main() {
    let par = Par::novo(42, "Rust");
    par.mostrar();
}

Em enums:

enum Resultado<T, E>
where
    T: Debug,
    E: Debug,
{
    Sucesso(T),
    Erro(E),
}

5. Bounds em Implementações de Traits

Podemos implementar traits condicionalmente usando bounds:

use std::fmt::Display;

struct Caixa<T>(T);

impl<T: Display> Display for Caixa<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Caixa({})", self.0)
    }
}

fn main() {
    let caixa = Caixa(100);
    println!("{}", caixa);
}

Where clauses em impl blocks trazem mais clareza:

trait Conversivel {
    fn converter(&self) -> String;
}

impl<T> Conversivel for Vec<T>
where
    T: std::fmt::Display,
{
    fn converter(&self) -> String {
        self.iter()
            .map(|item| item.to_string())
            .collect::<Vec<_>>()
            .join(", ")
    }
}

fn main() {
    let nums = vec![1, 2, 3];
    println!("{}", nums.converter());
}

6. Bounds em Associações de Tipos (Associated Types)

Tipos associados em traits podem ser restringidos com where clauses:

trait Container {
    type Item;

    fn obter(&self) -> Option<&Self::Item>;
    fn inserir(&mut self, item: Self::Item);
}

impl<T> Container for Vec<T>
where
    T: Clone + std::fmt::Debug,
{
    type Item = T;

    fn obter(&self) -> Option<&T> {
        self.first()
    }

    fn inserir(&mut self, item: T) {
        self.push(item);
    }
}

fn processar_container<C>(container: &mut C)
where
    C: Container,
    C::Item: std::fmt::Display,
{
    if let Some(item) = container.obter() {
        println!("Item: {}", item);
    }
}

fn main() {
    let mut vec = Vec::new();
    vec.inserir(42);
    processar_container(&mut vec);
}

7. Erros Comuns e Boas Práticas

Erro: esquecer bounds necessários

// Erro de compilação: método `clone` não disponível para T
fn clonar<T>(item: &T) -> T {
    item.clone() // error[E0599]: no method named `clone` found
}

// Correção:
fn clonar<T: Clone>(item: &T) -> T {
    item.clone()
}

Erro: bounds redundantes

// Redundante: Copy implica Clone
fn copiar<T: Clone + Copy>(item: T) -> T {
    item
}

// Melhor:
fn copiar<T: Copy>(item: T) -> T {
    item
}

Boas práticas essenciais:

  1. Prefira where clauses para funções com 3+ bounds
  2. Mantenha bounds mínimos necessários para evitar acoplamento excessivo
  3. Use traits marker para agrupar comportamentos relacionados
  4. Documente bounds complexos explicando por que são necessários

Exemplo de boa prática:

use std::fmt::Debug;

// Agrupando comportamentos
trait Persistivel: Debug + Clone + PartialEq {}

impl<T: Debug + Clone + PartialEq> Persistivel for T {}

fn salvar<T>(item: &T)
where
    T: Persistivel + serde::Serialize,
{
    println!("Salvando: {:?}", item);
}

Referências