Introdução a Generics em Rust
Programação genérica (generics) é um dos conceitos-chave do Rust, permitindo escrever código flexível, reutilizável e seguro. Se você está migrando para Rust a partir de outras linguagens (como C++ ou Java), certamente já encontrou templates ou genéricos. Em Rust, generics funcionam de forma similar, mas com ênfase especial em desempenho e segurança de memória.
Para que servem generics? Imagine que você precisa escrever uma função que encontra o elemento máximo em uma lista. Sem generics, você teria que escrever uma versão separada para i32, f64, String e cada tipo personalizado. Com generics, você escreve uma única função que funciona com qualquer tipo que suporte comparação.
Neste artigo, vamos explorar o que são generics em Rust, como usá-los com funções, structs e traits, além de analisar exemplos práticos.
Fundamentos de Generics: funções e structs
Funções genéricas
A maneira mais simples de conhecer generics é escrevendo uma função genérica. A sintaxe usa colchetes angulares <T>, onde T é um parâmetro de tipo (type parameter).
fn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!("Maior número: {}", result);
let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!("Maior caractere: {}", result);}No entanto, este código não compilará! O Rust exige que o tipo T implemente a trait PartialOrd (para comparação). As restrições (trait bounds) serão abordadas mais adiante; por enquanto, lembre-se: generics permitem abstrair o tipo concreto, mas o compilador precisa saber quais operações são permitidas.
Structs genéricas
Generics funcionam não apenas com funções, mas também com tipos de dados personalizados. Por exemplo, um ponto com coordenadas pode armazenar números inteiros ou de ponto flutuante:
struct Point<T> { x: T, y: T,}
fn main() { let integer_point = Point { x: 5, y: 10 }; let float_point = Point { x: 1.0, y: 4.0 }; println!("Ponto inteiro: ({}, {})", integer_point.x, integer_point.y); println!("Ponto flutuante: ({}, {})", float_point.x, float_point.y);}Você pode usar vários parâmetros de tipo se os campos tiverem tipos diferentes:
struct Pair<T, U> { first: T, second: U,}
fn main() { let pair = Pair { first: 42, second: "resposta" }; println!("Par: {} e {}", pair.first, pair.second);}Generics com traits e restrições (Trait Bounds)
O que são trait bounds?
Trait bounds são uma forma de informar ao compilador que um tipo genérico deve implementar determinadas traits. Sem eles, não podemos chamar métodos em um valor genérico. Vamos voltar à função largest e corrigi-la:
use std::cmp::PartialOrd;
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest}
fn main() { let numbers = vec![34, 50, 25, 100, 65]; println!("Maior: {}", largest(&numbers));}Agora a função aceita apenas tipos que suportam comparação (PartialOrd).
Múltiplas restrições
Você pode especificar várias traits usando +. Por exemplo, o tipo deve ser simultaneamente comparável e clonável:
use std::fmt::Display;
fn print_and_compare
```