Generics — Código que Funciona para Qualquer Tipo

Generics — Código que Funciona para Qualquer Tipo

Escrever a mesma função para cada tipo é o tipo de repetição que generics eliminam, com uma diferença em relação a C++: o corpo é verificado uma vez, contra os requisitos declarados. O artigo cobre funções e structs genéricas, a combinação com traits e por que o custo em execução é zero.
Rust

13 min de leitura

 

No artigo Traits — Definindo Comportamento Compartilhado 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.

Exercícios

Exercício 1

Escreva uma função genérica que devolva o maior elemento de uma fatia, e explique por que ela precisa de um bound.

Ver resposta

✓ Resposta:

fn maior<T: PartialOrd>(itens: &[T]) -> &T {
    let mut maior = &itens[0];
    for item in itens {
        if item > maior {
            maior = item;
        }
    }
    maior
}

Sem o bound PartialOrd, o compilador recusa a comparação: dentro de um genérico só é permitido o que o bound garante. Rust verifica o corpo do genérico uma vez, contra os requisitos declarados — diferente de C++, onde o erro só aparece na instanciação, e apontando para dentro do template.

Exercício 2

Defina uma struct genérica Par<T> e adicione um método disponível apenas quando T puder ser comparado.

Ver resposta

✓ Resposta:

struct Par<T> { a: T, b: T }

impl<T> Par<T> {
    fn novo(a: T, b: T) -> Self { Self { a, b } }
}

impl<T: PartialOrd + std::fmt::Display> Par<T> {
    fn maior(&self) {
        if self.a >= self.b { println!("{}", self.a) } else { println!("{}", self.b) }
    }
}

São dois blocos impl: o primeiro vale para qualquer T, o segundo só para os que satisfazem os bounds. Um Par de tipos não comparáveis continua existindo e construível — apenas não terá o método maior.

Exercício 3

Explique o que é monomorfização e por que se diz que generics em Rust têm custo zero em tempo de execução.

Ver resposta

✓ Resposta: Na compilação, o Rust gera uma cópia concreta do código genérico para cada tipo efetivamente usado — maior::<i32>, maior::<f64> e assim por diante. O binário final não tem genérico nenhum, apenas funções especializadas, então não há indireção nem verificação de tipo em execução: o desempenho é idêntico ao de escrever cada versão à mão. O custo existe, mas é de compilação e de tamanho do binário — a mesma troca que C++ faz com templates.

Exercício 4

Quando preferir Vec<Box<dyn Trait>> a um genérico com bound? Dê um caso de cada.

Ver resposta

✓ Resposta: Genérico quando todos os elementos são do mesmo tipo, decidido em tempo de compilação — uma função que ordena qualquer Vec<T: Ord>. Trait object quando tipos diferentes precisam conviver na mesma coleção ou quando o tipo só se conhece em execução — uma lista de plugins carregados por configuração, ou os elementos de uma interface gráfica. O genérico é mais rápido e permite mais verificações; o trait object é o único que resolve heterogeneidade, ao custo de uma chamada indireta.

Exercício 5

Implemente uma Pilha<T> genérica com empilhar, desempilhar e vazia, explicando o tipo de retorno de desempilhar.

Ver resposta

✓ Resposta:

struct Pilha<T> { itens: Vec<T> }

impl<T> Pilha<T> {
    fn nova() -> Self { Self { itens: Vec::new() } }
    fn empilhar(&mut self, item: T) { self.itens.push(item); }
    fn desempilhar(&mut self) -> Option<T> { self.itens.pop() }
    fn vazia(&self) -> bool { self.itens.is_empty() }
}

desempilhar devolve Option<T> porque a pilha pode estar vazia, e não existe valor sensato a inventar para esse caso. Quem chama é obrigado a tratar None — o contraste é com linguagens em que a operação equivalente lança exceção ou devolve lixo.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io
Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io

Publicar uma crate tem passos que não têm volta: o nome fica reservado e a…

Banco de Dados com SQLx — Persistência Real para sua API
Banco de Dados com SQLx — Persistência Real para sua API

O SQLx não esconde o banco atrás de objetos: o SQL continua SQL, e as macros o…

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Rust distingue o que executa do que produz valor, e essa distinção decide como…