Em Rust, genéricos são uma ferramenta poderosa para escrever código reutilizável, mas muitas vezes precisamos restringir quais tipos podem ser usados com um determinado genérico. É aí que entram os trait bounds: eles especificam que um tipo genérico deve implementar um ou mais traits. Nesta aula, vamos explorar as diferentes formas de aplicar trait bounds, incluindo a sintaxe where, a notação impl Trait e como combinar múltiplos bounds de maneira eficiente.

Dominar esses conceitos é essencial para criar APIs robustas e expressivas em Rust, permitindo que o compilador verifique restrições de tipo em tempo de compilação e garantindo que as funções genéricas funcionem apenas com tipos que atendam aos requisitos necessários.

Restrições em genéricos

Quando declaramos uma função ou struct genérica, podemos adicionar trait bounds após a declaração do parâmetro de tipo usando dois pontos (:) seguido do nome do trait. Por exemplo, fn exemplo<T: Display>(t: T) restringe T a tipos que implementam o trait Display. Isso permite que usemos métodos do trait dentro da função.

Essa abordagem é direta, mas pode se tornar verbosa quando há múltiplos parâmetros ou bounds complexos. Além disso, a sintaxe inline pode poluir a assinatura da função, especialmente quando os bounds são longos.

use std::fmt::Display;

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

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

No exemplo acima, a função imprimir aceita qualquer tipo que implemente Display. Se tentarmos passar um tipo que não implementa Display, o compilador emitirá um erro.

Sintaxe where

A sintaxe where permite escrever trait bounds separadamente da declaração dos parâmetros de tipo, melhorando a legibilidade, especialmente quando os bounds são longos ou envolvem vários parâmetros. A cláusula where é colocada antes do corpo da função e pode conter múltiplas condições.

use std::fmt::Debug;

fn comparar_e_imprimir<T, U>(a: T, b: U)
where
    T: Debug + PartialEq,
    U: Debug + PartialEq,
{
    if a == b {
        println!("{:?} é igual a {:?}", a, b);
    } else {
        println!("{:?} é diferente de {:?}", a, b);
    }
}

fn main() {
    comparar_e_imprimir(1, 2);
    comparar_e_imprimir("a", "a");
}

Aqui, ambos os tipos T e U devem implementar Debug e PartialEq. A cláusula where torna a assinatura mais limpa, especialmente se houvesse muitos parâmetros ou bounds aninhados.

Além disso, a sintaxe where é obrigatória para expressar certos bounds, como quando um bound depende de um tipo associado (ex: where T::Item: Clone).

impl Trait

O impl Trait é uma forma concisa de especificar que um parâmetro ou retorno de função implementa um trait, sem precisar nomear explicitamente o tipo genérico. Ele pode ser usado em posições de argumento (como uma forma de trait bound anônimo) e em posições de retorno (para ocultar o tipo concreto).

Em parâmetros, impl Trait é equivalente a um parâmetro genérico com aquele bound, mas não permite que o tipo seja referenciado pelo nome. É útil quando você não precisa do nome do tipo.

use std::fmt::Display;

fn saudacao(nome: impl Display) -> String {
    format!("Olá, {}!", nome)
}

fn main() {
    println!("{}", saudacao("Mundo"));
    println!("{}", saudacao(42));
}

No retorno, impl Trait permite retornar um tipo que implementa um trait sem expor o tipo concreto. Isso é útil para retornar closures ou iteradores, cujos tipos são frequentemente complexos ou impossíveis de nomear.

use std::fmt::Display;

fn criar_saudacao() -> impl Display {
    "Olá, mundo!"
}

fn main() {
    let msg = criar_saudacao();
    println!("{}", msg);
}

Importante: impl Trait em posição de retorno só pode ser usado se a função retornar um único tipo concreto (não pode retornar tipos diferentes condicionalmente).

Múltiplos bounds

Muitas vezes precisamos que um tipo implemente mais de um trait. Podemos combinar múltiplos bounds usando o operador +. Isso funciona tanto na sintaxe inline quanto na cláusula where.

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

fn exibir<T: Debug + Display>(valor: T) {
    println!("Debug: {:?}, Display: {}", valor, valor);
}

fn main() {
    exibir(42);
    exibir("texto");
}

Com a sintaxe where, ficaria:

fn exibir<T>(valor: T)
where
    T: Debug + Display,
{
    println!("Debug: {:?}, Display: {}", valor, valor);
}

Também é possível combinar impl Trait com múltiplos bounds usando +:

fn processar(valor: impl Debug + Display) {
    println!("{:?} - {}", valor, valor);
}

Além disso, podemos usar a sintaxe where para expressar bounds que envolvem tipos associados, como where T::Item: Clone. Isso é particularmente útil ao trabalhar com iteradores ou coleções.

fn clonar_itens<T>(itens: T) -> Vec<T::Item>
where
    T: IntoIterator,
    T::Item: Clone,
{
    itens.into_iter().collect()
}

fn main() {
    let v = vec![1, 2, 3];
    let clonados = clonar_itens(v);
    println!("{:?}", clonados);
}

Esse exemplo mostra como a cláusula where permite restringir tipos associados de forma clara.

Boas práticas

  • Prefira where para múltiplos bounds: quando você tem mais de dois bounds, use where para melhor legibilidade.
  • Use impl Trait para parâmetros simples: se você não precisa nomear o tipo, impl Trait é mais conciso.
  • Evite impl Trait em retornos de funções públicas: pois isso esconde o tipo concreto, o que pode dificultar o uso da API em alguns casos (ex: não é possível nomear o tipo retornado).
  • Combine bounds com + de forma ordenada: coloque primeiro os traits mais importantes ou mais específicos.
  • Documente os bounds: especialmente quando os requisitos não são óbvios, explique por que cada bound é necessário.

Referências

Exercícios

  1. Escreva uma função genérica maior que recebe dois valores do mesmo tipo e retorna o maior (usando PartialOrd). Use a sintaxe inline de trait bound.

    ✓ Resposta:
    fn maior<T: PartialOrd>(a: T, b: T) -> T {
        if a >= b { a } else { b }
    }
    
    fn main() {
        println!("{}", maior(10, 20));
        println!("{}", maior('a', 'z'));
    }
  2. Reescreva a função maior do exercício 1 usando a sintaxe where.

    ✓ Resposta:
    fn maior<T>(a: T, b: T) -> T
    where
        T: PartialOrd,
    {
        if a >= b { a } else { b }
    }
    
    fn main() {
        println!("{}", maior(10, 20));
    }
  3. Crie uma função exibir_dupla que recebe dois valores de tipos possivelmente diferentes, ambos implementando Display, e os imprime. Use impl Trait nos parâmetros.

    ✓ Resposta:
    use std::fmt::Display;
    
    fn exibir_dupla(a: impl Display, b: impl Display) {
        println!("{} e {}", a, b);
    }
    
    fn main() {
        exibir_dupla(1, "dois");
    }
  4. Escreva uma função processar que recebe um valor que implementa Debug e Clone, clona o valor e imprime tanto o original quanto o clone. Use múltiplos bounds com + e a sintaxe inline.

    ✓ Resposta:
    use std::fmt::Debug;
    
    fn processar<T: Debug + Clone>(valor: T) {
        let clone = valor.clone();
        println!("Original: {:?}", valor);
        println!("Clone: {:?}", clone);
    }
    
    fn main() {
        processar("texto".to_string());
    }
  5. Crie uma função somar_itens que recebe um iterável de números (que implementam IntoIterator com itens que implementam Add e Default) e retorna a soma. Use a cláusula where com tipos associados. Dica: use std::ops::Add e std::iter::Sum (ou implemente manualmente).

    ✓ Resposta:
    use std::ops::Add;
    
    fn somar_itens<T, I>(itens: T) -> I
    where
        T: IntoIterator<Item = I>,
        I: Add<Output = I> + Default + Copy,
    {
        let mut soma = I::default();
        for item in itens {
            soma = soma + item;
        }
        soma
    }
    
    fn main() {
        let v = vec![1, 2, 3];
        println!("{}", somar_itens(v));
    }