No artigo anterior aprendemos que traits definem o que um tipo pode fazer. Hoje vamos aprender como escrever código que funciona para qualquer tipo que satisfaça certas condições — sem duplicação, sem perda de desempenho. Isso é o que generics fazem em Rust.
Se você já usou Vec<T>, Option<T> ou Result<T, E>, já usou generics. Agora vamos entender como eles funcionam e como criar os seus próprios.
O problema que generics resolve
Imagine que você precisa de uma função que retorna o maior elemento de uma lista. Sem generics, você precisaria escrever uma versão para cada tipo:
fn maior_i32(lista: &[i32]) -> i32 {
let mut maior = lista[0];
for &item in lista {
if item > maior {
maior = item;
}
}
maior
}
fn maior_f64(lista: &[f64]) -> f64 {
let mut maior = lista[0];
for &item in lista {
if item > maior {
maior = item;
}
}
maior
}
O código é idêntico — apenas o tipo muda. Isso é duplicação desnecessária, difícil de manter. Generics eliminam esse problema.
Funções genéricas
A solução é parametrizar a função com um tipo genérico T:
fn maior<T: PartialOrd>(lista: &[T]) -> &T {
let mut maior = &lista[0];
for item in lista {
if item > maior {
maior = item;
}
}
maior
}
fn main() {
let numeros = vec![34, 50, 25, 100, 65];
println!("Maior inteiro: {}", maior(&numeros));
let decimais = vec![3.14, 2.71, 1.41, 1.73];
println!("Maior decimal: {}", maior(&decimais));
let letras = vec!['a', 'z', 'm', 'b'];
println!("Maior letra: {}", maior(&letras));
}
Saída:
Maior inteiro: 100
Maior decimal: 3.14
Maior letra: z
O T: PartialOrd é um trait bound — diz ao compilador que T deve implementar PartialOrd, ou seja, seus valores devem ser comparáveis. Sem esse bound, a linha item > maior não compilaria — o compilador não saberia se T suporta comparação.
Structs genéricas
Structs também podem ser parametrizadas por tipos:
#[derive(Debug)]
struct Par<T> {
primeiro: T,
segundo: T,
}
impl<T> Par<T> {
fn novo(primeiro: T, segundo: T) -> Self {
Par { primeiro, segundo }
}
fn primeiro(&self) -> &T {
&self.primeiro
}
fn segundo(&self) -> &T {
&self.segundo
}
}
// Métodos condicionais — só existem quando T implementa certos traits
impl<T: PartialOrd + std::fmt::Display> Par<T> {
fn maior(&self) -> &T {
if self.primeiro > self.segundo {
&self.primeiro
} else {
&self.segundo
}
}
fn exibir_maior(&self) {
println!("O maior é: {}", self.maior());
}
}
fn main() {
let par_numeros = Par::novo(5, 10);
let par_textos = Par::novo("abacaxi", "banana");
let par_floats = Par::novo(3.14, 2.71);
println!("{:?}", par_numeros);
par_numeros.exibir_maior();
println!("{:?}", par_textos);
par_textos.exibir_maior();
par_floats.exibir_maior();
}
Saída:
Par { primeiro: 5, segundo: 10 }
O maior é: 10
Par { primeiro: "abacaxi", segundo: "banana" }
O maior é: banana
O maior é: 3.14
Note o impl<T: PartialOrd + std::fmt::Display> Par<T> — esses métodos só existem para tipos T que implementam ambos os traits. Para um Par<Vec<i32>>, por exemplo, exibir_maior simplesmente não existiria, pois Vec não implementa PartialOrd.
Structs com múltiplos tipos genéricos
Structs podem ter vários parâmetros de tipo independentes:
#[derive(Debug)]
struct Mapa<K, V> {
chave: K,
valor: V,
}
impl<K: std::fmt::Display, V: std::fmt::Display> Mapa<K, V> {
fn novo(chave: K, valor: V) -> Self {
Mapa { chave, valor }
}
fn exibir(&self) {
println!("{} → {}", self.chave, self.valor);
}
}
fn main() {
let m1 = Mapa::novo("nome", "Ana");
let m2 = Mapa::novo(42u32, 3.14f64);
let m3 = Mapa::novo("ativo", true);
m1.exibir();
m2.exibir();
m3.exibir();
}
Saída:
nome → Ana
42 → 3.14
ativo → true
Enums genéricos
Já usamos enums genéricos desde o início — Option<T> e Result<T, E> são os exemplos mais proeminentes. Mas você pode criar os seus:
#[derive(Debug)]
enum Resultado<T, E> {
Sucesso(T),
Falha(E),
Pendente,
}
impl<T: std::fmt::Display, E: std::fmt::Display> Resultado<T, E> {
fn exibir(&self) {
match self {
Resultado::Sucesso(v) => println!("✓ Sucesso: {v}"),
Resultado::Falha(e) => println!("✗ Falha: {e}"),
Resultado::Pendente => println!("… Pendente"),
}
}
fn foi_bem_sucedido(&self) -> bool {
matches!(self, Resultado::Sucesso(_))
}
}
fn main() {
let r1: Resultado<String, String> =
Resultado::Sucesso(String::from("Dados processados"));
let r2: Resultado<i32, String> =
Resultado::Falha(String::from("Timeout na conexão"));
let r3: Resultado<f64, String> = Resultado::Pendente;
r1.exibir();
r2.exibir();
r3.exibir();
println!("r1 bem-sucedido? {}", r1.foi_bem_sucedido());
println!("r2 bem-sucedido? {}", r2.foi_bem_sucedido());
}
Saída:
✓ Sucesso: Dados processados
✗ Falha: Timeout na conexão
… Pendente
true
false
Generics e traits juntos — onde o poder real aparece
A combinação de generics com traits permite criar abstrações extremamente expressivas. Vamos ver um exemplo mais elaborado — uma função de pipeline que aplica transformações em sequência:
fn pipeline<T, U, V, F, G>(valor: T, f: F, g: G) -> V
where
F: Fn(T) -> U,
G: Fn(U) -> V,
{
g(f(valor))
}
fn main() {
// Pipeline: String → usize → String
let resultado = pipeline(
String::from(" Olá, Rust! "),
|s: String| s.trim().to_string(),
|s: String| format!("Processado: '{s}' ({} chars)", s.len()),
);
println!("{resultado}");
// Pipeline: i32 → f64 → String
let resultado2 = pipeline(
42i32,
|n| n as f64 * 1.5,
|f: f64| format!("{f:.2}"),
);
println!("{resultado2}");
}
Saída:
Processado: 'Olá, Rust!' (10 chars)
63.00
Implementando uma estrutura genérica completa: Pilha
Vamos implementar uma pilha genérica — uma estrutura de dados clássica:
#[derive(Debug)]
struct Pilha<T> {
elementos: Vec<T>,
capacidade_maxima: usize,
}
impl<T> Pilha<T> {
fn nova(capacidade_maxima: usize) -> Self {
Pilha {
elementos: Vec::new(),
capacidade_maxima,
}
}
fn empurrar(&mut self, item: T) -> Result<(), String> {
if self.elementos.len() >= self.capacidade_maxima {
Err(format!(
"Pilha cheia — capacidade máxima: {}",
self.capacidade_maxima
))
} else {
self.elementos.push(item);
Ok(())
}
}
fn retirar(&mut self) -> Option<T> {
self.elementos.pop()
}
fn topo(&self) -> Option<&T> {
self.elementos.last()
}
fn esta_vazia(&self) -> bool {
self.elementos.is_empty()
}
fn esta_cheia(&self) -> bool {
self.elementos.len() >= self.capacidade_maxima
}
fn tamanho(&self) -> usize {
self.elementos.len()
}
}
// Método extra apenas para tipos que implementam Display
impl<T: std::fmt::Display> Pilha<T> {
fn exibir(&self) {
if self.esta_vazia() {
println!("Pilha vazia");
return;
}
println!("Pilha ({}/{}):", self.tamanho(), self.capacidade_maxima);
for (i, elem) in self.elementos.iter().rev().enumerate() {
let marcador = if i == 0 { " ← topo" } else { "" };
println!(" {elem}{marcador}");
}
}
}
fn main() {
println!("── Pilha de inteiros ──");
let mut pilha: Pilha<i32> = Pilha::nova(4);
for n in [10, 20, 30, 40] {
match pilha.empurrar(n) {
Ok(()) => println!("Empurrado: {n}"),
Err(msg) => println!("Erro: {msg}"),
}
}
// Tenta ultrapassar a capacidade
match pilha.empurrar(50) {
Ok(()) => println!("Empurrado: 50"),
Err(msg) => println!("Erro: {msg}"),
}
pilha.exibir();
println!("\nRetirando elementos:");
while let Some(topo) = pilha.retirar() {
println!(" Retirado: {topo}");
}
println!("\n── Pilha de strings ──");
let mut pilha_str: Pilha<String> = Pilha::nova(3);
pilha_str.empurrar(String::from("primeiro")).unwrap();
pilha_str.empurrar(String::from("segundo")).unwrap();
pilha_str.empurrar(String::from("terceiro")).unwrap();
pilha_str.exibir();
}
Saída:
── Pilha de inteiros ──
Empurrado: 10
Empurrado: 20
Empurrado: 30
Empurrado: 40
Erro: Pilha cheia — capacidade máxima: 4
Pilha (4/4):
40 ← topo
30
20
10
Retirando elementos:
Retirado: 40
Retirado: 30
Retirado: 20
Retirado: 10
── Pilha de strings ──
Pilha (3/3):
terceiro ← topo
segundo
primeiro
Custo zero em tempo de execução
Uma das propriedades mais importantes de generics em Rust é a monomorfização: o compilador gera código especializado para cada tipo concreto que você usa com um tipo genérico. Isso acontece em tempo de compilação.
// Você escreve isso uma vez:
fn maior<T: PartialOrd>(lista: &[T]) -> &T { ... }
// O compilador gera algo equivalente a:
fn maior_i32(lista: &[i32]) -> &i32 { ... }
fn maior_f64(lista: &[f64]) -> &f64 { ... }
fn maior_char(lista: &[char]) -> &char { ... }
Não há boxing, não há dispatch dinâmico, não há custo em tempo de execução. O código genérico é tão eficiente quanto código especializado escrito manualmente — você ganha abstração sem pagar nada em desempenho.
Isso contrasta com trait objects (dyn Trait), que usam dispatch dinâmico e têm um pequeno custo em execução. A escolha entre generics e trait objects é uma escolha entre desempenho máximo e flexibilidade de tipos em tempo de execução.
Type aliases — nomeando tipos complexos
Quando tipos genéricos ficam complexos, aliases de tipo tornam o código legível:
type Resultado<T> = Result<T, String>;
type MatrizF64 = Vec<Vec<f64>>;
type Cache<K, V> = std::collections::HashMap<K, Vec<V>>;
fn processar(dados: &[i32]) -> Resultado<i32> {
if dados.is_empty() {
Err(String::from("Lista vazia"))
} else {
Ok(dados.iter().sum())
}
}
fn main() {
let dados = vec![1, 2, 3, 4, 5];
match processar(&dados) {
Ok(soma) => println!("Soma: {soma}"),
Err(e) => println!("Erro: {e}"),
}
let matriz: MatrizF64 = vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
];
for linha in &matriz {
let soma: f64 = linha.iter().sum();
println!("{:?} → soma: {soma}", linha);
}
}
Quando usar generics vs trait objects
A decisão entre generics e trait objects é uma das mais comuns em Rust:
Use generics quando os tipos são conhecidos em tempo de compilação, quando o desempenho é crítico, quando você quer que o compilador gere código especializado. É a escolha padrão para a maioria das situações.
Use trait objects (dyn Trait) quando você precisa de uma coleção com tipos diferentes em tempo de execução, quando está construindo sistemas de plugins extensíveis, ou quando o tipo concreto é genuinamente desconhecido até a execução.
// Generics — dispatch estático, mais rápido
fn processar_generico<T: Forma>(forma: &T) -> f64 {
forma.area()
}
// Trait object — dispatch dinâmico, mais flexível
fn processar_dinamico(forma: &dyn Forma) -> f64 {
forma.area()
}
Na prática, generics são a escolha certa em 80% dos casos. Trait objects entram quando você precisa de heterogeneidade genuína em tempo de execução.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 10 — Generic Types, Traits, and Lifetimes — https://doc.rust-lang.org/book/ch10-00-generics.html
- Rust by Example — Generics — https://doc.rust-lang.org/rust-by-example/generics.html
- Rust Reference — Generic parameters — https://doc.rust-lang.org/reference/items/generics.html
- "Rust Generics" — Logan Smith — vídeo explicando monomorfização em profundidade — https://www.youtube.com/watch?v=6rcTSxPJ6Bw
- "Zero Cost Abstractions" — Without Boats — artigo sobre como Rust garante custo zero — https://without.boats/blog/zero-cost-abstractions/
- Rustlings, seção
generics— https://github.com/rust-lang/rustlings