Trait bounds e where
Esta aula aborda como restringir tipos genéricos em Rust usando trait bounds, incluindo a sintaxe 'where' para maior clareza, o uso de 'impl Trait' em parâmetros e retornos, e a combinação de múltiplos bounds. O conteúdo inclui exemplos práticos e boas práticas para escrever código genérico mais legível e seguro.
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
wherepara múltiplos bounds: quando você tem mais de dois bounds, usewherepara melhor legibilidade. - Use
impl Traitpara parâmetros simples: se você não precisa nomear o tipo,impl Traité mais conciso. - Evite
impl Traitem 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
- The Rust Book: Traits
- Rust Reference: Generics
- Rust by Example: Bounds
- Rust by Example: Multiple bounds
- Rust by Example: Where clauses
- Rust by Example: Closures (impl Trait)
Exercícios
-
Escreva uma função genérica
maiorque recebe dois valores do mesmo tipo e retorna o maior (usandoPartialOrd). 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')); } -
Reescreva a função
maiordo exercício 1 usando a sintaxewhere.✓ Resposta:fn maior<T>(a: T, b: T) -> T where T: PartialOrd, { if a >= b { a } else { b } } fn main() { println!("{}", maior(10, 20)); } -
Crie uma função
exibir_duplaque recebe dois valores de tipos possivelmente diferentes, ambos implementandoDisplay, e os imprime. Useimpl Traitnos 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"); } -
Escreva uma função
processarque recebe um valor que implementaDebugeClone, 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()); } -
Crie uma função
somar_itensque recebe um iterável de números (que implementamIntoIteratorcom itens que implementamAddeDefault) e retorna a soma. Use a cláusulawherecom tipos associados. Dica: usestd::ops::Addestd::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)); }