Introdução aos Generics em Rust
Generics são um dos pilares da programação moderna e Rust oferece uma implementação particularmente robusta e segura. Em essência, generics permitem que você escreva código parametrizado por tipo, eliminando duplicação sem comprometer a segurança de tipos. Diferentemente de linguagens como Java ou C#, Rust realiza a monomorphização em tempo de compilação: para cada tipo concreto usado com um generic, o compilador gera uma cópia especializada do código. Isso significa zero custo em runtime e máxima performance.
Neste artigo, exploraremos como trabalhar com generics em funções e structs, começando pelos conceitos fundamentais até padrões avançados. Você aprenderá a aproveitar o poder dos generics para escrever código reutilizável, type-safe e eficiente.
Generics em Funções
Sintaxe Básica
Funções genéricas recebem um parâmetro de tipo entre colchetes angulares <T>. O identificador T é uma convenção (como em Java), mas você pode usar qualquer nome válido. Veja este exemplo simples:
fn maior<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
println!("{}", maior(10, 20)); // 20
println!("{}", maior(3.14, 2.71)); // 3.14
println!("{}", maior("apple", "zebra")); // "zebra"
}
Aqui, T: std::cmp::PartialOrd é um trait bound — uma restrição indicando que T deve implementar a trait PartialOrd (comparação). Sem isso, o compilador não saberia como comparar dois valores de tipo T.
Múltiplos Parâmetros de Tipo
Você pode parametrizar uma função com vários tipos simultaneamente. Isso é especialmente útil para operações que envolvem tipos diferentes:
fn tupla_invertida<T, U>(a: T, b: U) -> (U, T) {
(b, a)
}
fn main() {
let resultado = tupla_invertida(42, "Rust");
println!("{:?}", resultado); // ("Rust", 42)
}
Cada parâmetro de tipo é independente e pode ter suas próprias restrições. Você pode combinar múltiplos trait bounds usando +:
fn processar<T: std::fmt::Display + std::clone::Clone>(valor: T) {
let copia = valor.clone();
println!("Original: {}, Cópia: {}", valor, copia);
}
fn main() {
processar(String::from("Olá, Rust!"));
}
Generics em Structs
Definindo Structs Genéricas
Structs genéricas armazenam valores de tipo parametrizado. Isso é fundamental para criar contêineres reutilizáveis:
struct Caixa<T> {
conteudo: T,
}
impl<T> Caixa<T> {
fn novo(conteudo: T) -> Caixa<T> {
Caixa { conteudo }
}
fn obter_conteudo(&self) -> &T {
&self.conteudo
}
fn consumir(self) -> T {
self.conteudo
}
}
fn main() {
let caixa_int = Caixa::novo(42);
let caixa_str = Caixa::novo("Rust é incrível");
println!("{}", caixa_int.obter_conteudo());
println!("{}", caixa_str.obter_conteudo());
let valor = caixa_int.consumir();
println!("Consumido: {}", valor);
}
Observe que impl<T> indica que os métodos funcionam para qualquer tipo T. A sintaxe Caixa<T>::novo() retorna uma instância parametrizada.
Structs com Múltiplos Generics
Assim como funções, structs podem ter vários parâmetros de tipo:
struct Par<T, U> {
primeiro: T,
segundo: U,
}
impl<T: std::fmt::Debug, U: std::fmt::Debug> Par<T, U> {
fn exibir(&self) {
println!("Primeiro: {:?}, Segundo: {:?}", self.primeiro, self.segundo);
}
}
fn main() {
let par = Par {
primeiro: 10,
segundo: "texto",
};
par.exibir();
}
Aqui, o trait bound Debug é aplicado apenas aos métodos que precisam exibir os valores. Outras operações na struct podem não exigir essa restrição.
Trait Bounds e Implementações Especializadas
Restrições Avançadas
Rust permite definir implementações especializadas para casos específicos. Isso é chamado de specialization (ainda em fase experimental) ou uso estratégico de trait bounds:
struct Conteiner<T> {
items: Vec<T>,
}
// Implementação geral
impl<T> Conteiner<T> {
fn novo() -> Self {
Conteiner { items: Vec::new() }
}
fn adicionar(&mut self, item: T) {
self.items.push(item);
}
}
// Implementação especializada para tipos Clone
impl<T: Clone> Conteiner<T> {
fn duplicar_primeiro(&mut self) {
if let Some(primeiro) = self.items.first() {
self.items.push(primeiro.clone());
}
}
}
fn main() {
let mut cont = Conteiner::novo();
cont.adicionar(5);
cont.adicionar(10);
cont.duplicar_primeiro();
println!("{:?}", cont.items); // [5, 10, 5]
}
A implementação de duplicar_primeiro() está disponível apenas se T implementa Clone. Isso oferece flexibilidade sem comprometer a segurança.
Where Clauses
Para trait bounds complexos, a syntax where melhora a legibilidade:
fn processar_pares<T, U>(a: T, b: U)
where
T: std::fmt::Display + std::cmp::PartialOrd<U>,
U: std::fmt::Display,
{
println!("Processando: {} e {}", a, b);
}
A cláusula where deixa claro quais restrições se aplicam a cada tipo, especialmente útil em assinaturas complexas.
Conclusão
Generics em Rust permitem escrever código altamente reutilizável mantendo segurança de tipos total. Os três pontos-chave são: (1) funções genéricas reduzem duplicação parametrizando operações comuns; (2) structs genéricas criam contêineres flexíveis que funcionam com qualquer tipo, da mesma forma que Vec<T> e Option<T>; (3) trait bounds garantem que operações específicas estejam disponíveis, evitando erros em tempo de compilação. Dominando generics, você terá as ferramentas para escrever bibliotecas robustas e extensíveis.