Rust

Generics — Código que Funciona para Qualquer Tipo Já leu

10 min de leitura

Generics — Código que Funciona para Qualquer Tipo
  No artigo anterior aprendemos que traits definem o que um tipo pode fazer. Hoje vamos aprender como escrever código que funciona para qualqu

 

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

Comentários

Mais em Rust

Smart Pointers — Box, Rc e RefCell
Smart Pointers — Box, Rc e RefCell

&nbsp; At&eacute; agora trabalhamos com dados na stack e refer&ecirc;ncias s...

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...

Por que Rust existe e o que ele quer de você
Por que Rust existe e o que ele quer de você

Toda linguagem de programa&ccedil;&atilde;o nasce de uma frustra&ccedil;&atil...