Generics em Rust: para que servem e como usar
Generics (genéricos) — um dos conceitos-chave do Rust, que permite escrever código flexível, reutilizável e seguro. Se você está começando a aprender Rust, entender os genéricos abrirá caminho para escrever bibliotecas e aplicações eficientes. Neste artigo, vamos explicar o que são Generics, para que servem e como aplicá-los na prática.
O que são Generics e para que servem?
Imagine que você precisa escrever uma função que encontra o elemento máximo em um array. Para números, uma implementação; para strings, outra. Para não copiar código para cada tipo, o Rust oferece programação genérica com Generics.
Genéricos permitem definir estruturas, funções, enumerações ou métodos que funcionam com qualquer tipo de dado, sem se prender a um específico. O compilador, durante a compilação, substitui pelo tipo real — esse processo é chamado de monomorfização.
Sem genéricos, você escreveria:
fn max_i32(list: &[i32]) -> i32 { let mut max = list[0]; for &item in list { if item > max { max = item; } } max}
fn max_f64(list: &[f64]) -> f64 { let mut max = list[0]; for &item in list { if item > max { max = item; } } max}Com genéricos, isso se transforma em uma única função universal.
Fundamentos do uso de Generics
Funções genéricas
Para tornar uma função genérica, é necessário especificar um parâmetro de tipo entre colchetes angulares <T>:
fn max<T>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Mas esse código não compilará — o compilador dirá que para o tipo T não foi implementado o trait PartialOrd. É aqui que entram as restrições de traits.
Estruturas e enumerações genéricas
Estruturas também podem ser genéricas:
struct Point<T> { x: T, y: T,}
fn main() { let int_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.5, y: 3.8 };}Você pode especificar vários parâmetros de tipo:
struct Pair<T, U> { first: T, second: U,}Enumerações também suportam genéricos — lembre-se de Option<T> ou Result<T, E> da biblioteca padrão.
Métodos genéricos
Para estruturas com genéricos, é possível definir métodos:
impl<T> Point<T> { fn x(&self) -> &T { &self.x }}Ou apenas para um tipo específico:
impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() }}Restrições de traits (Trait Bounds)
Para usar operações de comparação, adição ou outros métodos, é necessário especificar quais traits o tipo implementa. Voltemos à função max:
fn max<T: PartialOrd>(list: &[T]) -> T { let mut max = &list[0]; for item in list { if item > max { max = item; } } *max}Agora o código compila — dissemos que T deve suportar ordenação parcial (PartialOrd).
Você pode combinar traits com +:
fn clone_and_compare<T: Clone + PartialOrd>(a: T, b: T) -> bool { a.clone() > b.clone()}Ou usar a sintaxe where para legibilidade:
fn some_function<T, U>(t: &T, u: &U) -> i32where T: