Generics em funções e structs
1. Introdução aos Generics em Rust
Generics são uma das ferramentas mais poderosas do sistema de tipos de Rust. Eles permitem escrever código que funciona com múltiplos tipos concretos, mantendo a segurança de tipos em tempo de compilação. Em vez de duplicar funções ou structs para cada tipo específico, você escreve uma única implementação genérica que pode ser reutilizada.
A principal motivação para usar generics é o reuso de código combinado com type safety. Diferente de linguagens dinâmicas, Rust verifica em tempo de compilação se todos os usos do tipo genérico são válidos, prevenindo erros em execução.
A sintaxe básica envolve declarar parâmetros de tipo entre colchetes angulares <T>. Por convenção, usamos nomes curtos como T, U, V para tipos genéricos.
// Exemplo conceitual - ainda não compila sem trait bounds
fn retornar_mesmo<T>(valor: T) -> T {
valor
}
fn main() {
let numero = retornar_mesmo(42);
let texto = retornar_mesmo("Olá");
println!("{} {}", numero, texto);
}
2. Definindo Funções Genéricas
Vamos criar uma função que encontra o maior entre dois valores. Para isso, precisamos que o tipo T implemente as traits PartialOrd (para comparação) e Copy (para cópia implícita):
fn maior<T: PartialOrd + Copy>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("Maior inteiro: {}", maior(10, 20));
println!("Maior float: {}", maior(3.14, 2.71));
println!("Maior char: {}", maior('z', 'a'));
}
Funções podem ter múltiplos parâmetros de tipo independentes:
fn misturar<T, U>(a: T, b: U) -> String {
format!("{:?} e {:?}", a, b)
}
fn main() {
println!("{}", misturar(42, "resposta"));
println!("{}", misturar(true, 3.14));
}
Note que {:?} requer a trait Debug. Para tipos que não implementam Debug, precisamos adicionar trait bounds explícitas:
use std::fmt::Debug;
fn misturar_com_debug<T: Debug, U: Debug>(a: T, b: U) -> String {
format!("{:?} e {:?}", a, b)
}
3. Definindo Structs Genéricas
Structs genéricas permitem criar tipos flexíveis que armazenam dados de qualquer tipo:
struct Ponto<T> {
x: T,
y: T,
}
struct Par<T, U> {
primeiro: T,
segundo: U,
}
fn main() {
let ponto_inteiro = Ponto { x: 5, y: 10 };
let ponto_float = Ponto { x: 1.5, y: 3.2 };
let par_misto = Par { primeiro: "idade", segundo: 30 };
println!("Ponto: ({}, {})", ponto_inteiro.x, ponto_inteiro.y);
println!("Par: {} = {}", par_misto.primeiro, par_misto.segundo);
}
Implementamos métodos em structs genéricas usando impl<T>:
impl<T> Ponto<T> {
fn new(x: T, y: T) -> Self {
Ponto { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Ponto::new(10, 20);
println!("x = {}", p.x());
}
4. Implementando Métodos em Tipos Genéricos
Podemos introduzir novos parâmetros genéricos nos métodos:
struct Ponto<T> {
x: T,
y: T,
}
impl<T> Ponto<T> {
// Método que usa o mesmo tipo T
fn distancia_quadrada(&self) -> f64
where T: Into<f64> + Copy
{
let x: f64 = self.x.into();
let y: f64 = self.y.into();
(x * x + y * y).sqrt()
}
// Método que introduz novo tipo genérico U
fn converter<U>(self) -> Ponto<U>
where T: Into<U>
{
Ponto {
x: self.x.into(),
y: self.y.into(),
}
}
}
fn main() {
let p1 = Ponto { x: 3, y: 4 };
println!("Distância: {}", p1.distancia_quadrada());
let p2 = p1.converter::<f64>();
println!("Convertido: ({}, {})", p2.x, p2.y);
}
Também podemos ter blocos impl específicos para tipos concretos:
impl Ponto<f64> {
fn magnitude(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let p = Ponto { x: 3.0, y: 4.0 };
println!("Magnitude: {}", p.magnitude());
}
5. Trait Bounds e Where Clauses em Generics
Trait bounds restringem quais tipos podem ser usados com generics. A sintaxe where melhora a legibilidade em declarações complexas:
use std::fmt::Display;
// Sintaxe direta
fn imprimir_display<T: Display>(valor: T) {
println!("{}", valor);
}
// Usando where para múltiplos bounds
fn processar<T, U>(a: T, b: U) -> String
where
T: Display + Clone,
U: Display + PartialOrd,
{
format!("{} > {} = {}", a, b, a > b)
}
fn main() {
imprimir_display("Olá mundo");
println!("{}", processar(10, 5));
}
Where clauses são especialmente úteis quando os bounds se tornam complexos:
use std::fmt::Debug;
fn funcao_complexa<T, U>(a: T, b: U)
where
T: Debug + Clone + PartialEq,
U: Debug + Clone + PartialOrd,
T::Output: Display, // Exemplo de bound associado
{
println!("{:?} e {:?}", a, b);
}
6. Monomorfização e Performance
A monomorfização é o processo pelo qual o compilador Rust gera código específico para cada tipo concreto usado com generics. Isso significa que generics em Rust são uma abstração de custo zero — não há overhead em tempo de execução.
fn identidade<T>(x: T) -> T { x }
// O compilador gera equivalentes a:
fn identidade_i32(x: i32) -> i32 { x }
fn identidade_f64(x: f64) -> f64 { x }
fn identidade_string(x: String) -> String { x }
Vantagens:
- Performance igual a código escrito manualmente para cada tipo
- Otimizações específicas por tipo (inline, especialização)
- Sem dispatch dinâmico em tempo de execução
Trade-offs:
- Aumento do tamanho do binário (cada tipo gera código duplicado)
- Maior tempo de compilação
fn main() {
// Cada uso concreto gera código especializado
let a = identidade(42);
let b = identidade(3.14);
let c = identidade(String::from("teste"));
println!("{} {} {}", a, b, c);
}
7. Casos Avançados e Boas Práticas
Generics com lifetimes exigem sintaxe especial para garantir que referências vivam o suficiente:
struct Referencia<'a, T: 'a> {
valor: &'a T,
}
fn processar_referencia<'a, T: 'a + Display>(refe: &'a T) {
println!("{}", refe);
}
fn main() {
let numero = 42;
let r = Referencia { valor: &numero };
processar_referencia(r.valor);
}
Comparação entre generics e trait objects:
- Generics: monomorfização, zero-cost, dispatch estático
- Trait objects (dyn Trait): dispatch dinâmico, tamanho fixo, flexibilidade em coleções heterogêneas
Boas práticas:
1. Use generics quando o tipo é conhecido em tempo de compilação
2. Prefira generics para funções que operam em um único tipo por chamada
3. Use trait objects quando precisar de coleções heterogêneas
4. Evite generics excessivos que dificultam a leitura
5. Documente os trait bounds necessários
// Exemplo de decisão de design
use std::fmt::Display;
// Generics - melhor performance, tipos homogêneos
fn imprimir_lista<T: Display>(itens: Vec<T>) {
for item in itens {
println!("{}", item);
}
}
// Trait objects - flexibilidade, coleções heterogêneas
fn imprimir_lista_dinamica(itens: Vec<Box<dyn Display>>) {
for item in itens {
println!("{}", item);
}
}
fn main() {
imprimir_lista(vec![1, 2, 3]);
imprimir_lista_dinamica(vec![
Box::new(42),
Box::new("texto"),
Box::new(3.14),
]);
}
Generics são fundamentais para escrever código Rust idiomático, eficiente e reutilizável. Dominá-los permite criar bibliotecas flexíveis sem sacrificar performance ou segurança de tipos.
Referências
- The Rust Programming Language - Chapter 10: Generic Types, Traits, and Lifetimes — Capítulo oficial do livro que cobre generics, traits e lifetimes em profundidade
- Rust by Example - Generics — Exemplos práticos interativos de generics em funções, structs e métodos
- Rust Reference - Generic Parameters — Documentação de referência oficial sobre parâmetros genéricos e where clauses
- Rustnomicon - Monomorphization — Explicação detalhada sobre monomorfização e suas implicações
- Rust Design Patterns - Generics vs Trait Objects — Guia prático sobre quando usar generics versus trait objects