Generics em Rust: para que servem e como usar na prática
Programação genérica (Generics) é um dos recursos-chave do Rust, que permite escrever código flexível, reutilizável e seguro. Neste artigo, vamos explorar para que servem os Generics, como funcionam internamente e como aplicá-los em projetos reais. O material será útil tanto para iniciantes que estão aprendendo Rust quanto para desenvolvedores experientes que desejam sistematizar seus conhecimentos.
Para que servem os Generics em Rust?
Imagine que você precisa escrever uma função que encontra o maior elemento em um array. Sem Generics, você teria que criar versões separadas para cada tipo de dado: i32, f64, String e assim por diante. Isso leva à duplicação de código e dificulta a manutenção.
Generics resolvem esse problema, permitindo parametrizar tipos. Em vez de escrever:
fn find_max_i32(list: &[i32]) -> i32 { ... }fn find_max_f64(list: &[f64]) -> f64 { ... }Você escreve uma única função universal:
fn find_max<T: PartialOrd>(list: &[T]) -> &T { ... }Principais vantagens dos Generics:
- Reutilização de código — uma função ou estrutura funciona com diferentes tipos
- Segurança de tipos em tempo de compilação — todos os erros de tipo são detectados antes da execução do programa
- Desempenho — Rust usa monomorfização, criando código otimizado para cada tipo específico
- Legibilidade — menos duplicação, o código se torna mais conciso
Como funcionam os Generics: monomorfização
Rust compila código genérico de uma maneira especial — através da monomorfização. Isso significa que o compilador cria uma cópia separada da função ou estrutura para cada tipo específico usado no programa.
Exemplo:
fn print_type<T>(val: T) { println!("Valor: {}", val);}
fn main() { print_type(42); // T = i32 print_type(3.14); // T = f64 print_type("Olá"); // T = &str}Após a compilação, Rust criará três versões de print_type:
print_type_i32print_type_f64print_type_str
Isso garante que o código genérico não perde desempenho em relação ao código especializado. A única "desvantagem" é o aumento do tamanho do arquivo binário, mas na prática isso raramente se torna um problema.
Uso de Generics com funções e estruturas
Generics em funções
A maneira mais simples de usar Generics é parametrizar uma função:
fn swap<T>(a: &mut T, b: &mut T) { let temp = std::mem::replace(a, unsafe { std::mem::zeroed() }); *a = std::mem::replace(b, temp); *b = temp;}
fn main() { let mut x = 10; let mut y = 20; swap(&mut x, &mut y); println!("x = {}, y = {}", x, y); // x = 20, y = 10}Observe: não especificamos o tipo — o compilador o infere automaticamente com base nos argumentos.
Generics em estruturas
Estruturas também podem ser genéricas. Isso é especialmente útil para contêineres de dados:
struct Point<T> { x: T, y: T,}
impl<T> Point<T> { fn new(x: T, y: T) -> Self { Point { x, y } }
fn distance_from_origin(&self) -> f64 where T: Into<f64> + Copy, { let x: f64 = self.x.into(); let y: f64 = self.y.into(); (x * x + y * y).sqrt() }}
fn main() { let int_point = Point::new(3, 4); let float_point = Point::new(3.0, 4.0); // distance_from_origin funciona apenas para tipos que implementam Into<f6