Rust Admin

Generics em Rust: Funções e Structs Parametrizadas por Tipo: Do Básico ao Avançado Já leu

Introdução aos Generics em Rust Generics são um dos pilares da programação moderna e Rust oferece uma implementação particularmente robusta e segura. Em essência, generics permitem que você escreva código parametrizado por tipo, eliminando duplicação sem comprometer a segurança de tipos. Diferentemente de linguagens como Java ou C#, Rust realiza a monomorphização em tempo de compilação: para cada tipo concreto usado com um generic, o compilador gera uma cópia especializada do código. Isso significa zero custo em runtime e máxima performance. Neste artigo, exploraremos como trabalhar com generics em funções e structs, começando pelos conceitos fundamentais até padrões avançados. Você aprenderá a aproveitar o poder dos generics para escrever código reutilizável, type-safe e eficiente. Generics em Funções Sintaxe Básica Funções genéricas recebem um parâmetro de tipo entre colchetes angulares . O identificador é uma convenção (como em Java), mas você pode usar qualquer nome válido. Veja este exemplo simples: Aqui, é um trait bound — uma restrição indicando que deve

Introdução aos Generics em Rust

Generics são um dos pilares da programação moderna e Rust oferece uma implementação particularmente robusta e segura. Em essência, generics permitem que você escreva código parametrizado por tipo, eliminando duplicação sem comprometer a segurança de tipos. Diferentemente de linguagens como Java ou C#, Rust realiza a monomorphização em tempo de compilação: para cada tipo concreto usado com um generic, o compilador gera uma cópia especializada do código. Isso significa zero custo em runtime e máxima performance.

Neste artigo, exploraremos como trabalhar com generics em funções e structs, começando pelos conceitos fundamentais até padrões avançados. Você aprenderá a aproveitar o poder dos generics para escrever código reutilizável, type-safe e eficiente.

Generics em Funções

Sintaxe Básica

Funções genéricas recebem um parâmetro de tipo entre colchetes angulares <T>. O identificador T é uma convenção (como em Java), mas você pode usar qualquer nome válido. Veja este exemplo simples:

fn maior<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("{}", maior(10, 20));           // 20
    println!("{}", maior(3.14, 2.71));       // 3.14
    println!("{}", maior("apple", "zebra")); // "zebra"
}

Aqui, T: std::cmp::PartialOrd é um trait bound — uma restrição indicando que T deve implementar a trait PartialOrd (comparação). Sem isso, o compilador não saberia como comparar dois valores de tipo T.

Múltiplos Parâmetros de Tipo

Você pode parametrizar uma função com vários tipos simultaneamente. Isso é especialmente útil para operações que envolvem tipos diferentes:

fn tupla_invertida<T, U>(a: T, b: U) -> (U, T) {
    (b, a)
}

fn main() {
    let resultado = tupla_invertida(42, "Rust");
    println!("{:?}", resultado); // ("Rust", 42)
}

Cada parâmetro de tipo é independente e pode ter suas próprias restrições. Você pode combinar múltiplos trait bounds usando +:

fn processar<T: std::fmt::Display + std::clone::Clone>(valor: T) {
    let copia = valor.clone();
    println!("Original: {}, Cópia: {}", valor, copia);
}

fn main() {
    processar(String::from("Olá, Rust!"));
}

Generics em Structs

Definindo Structs Genéricas

Structs genéricas armazenam valores de tipo parametrizado. Isso é fundamental para criar contêineres reutilizáveis:

struct Caixa<T> {
    conteudo: T,
}

impl<T> Caixa<T> {
    fn novo(conteudo: T) -> Caixa<T> {
        Caixa { conteudo }
    }

    fn obter_conteudo(&self) -> &T {
        &self.conteudo
    }

    fn consumir(self) -> T {
        self.conteudo
    }
}

fn main() {
    let caixa_int = Caixa::novo(42);
    let caixa_str = Caixa::novo("Rust é incrível");

    println!("{}", caixa_int.obter_conteudo());
    println!("{}", caixa_str.obter_conteudo());

    let valor = caixa_int.consumir();
    println!("Consumido: {}", valor);
}

Observe que impl<T> indica que os métodos funcionam para qualquer tipo T. A sintaxe Caixa<T>::novo() retorna uma instância parametrizada.

Structs com Múltiplos Generics

Assim como funções, structs podem ter vários parâmetros de tipo:

struct Par<T, U> {
    primeiro: T,
    segundo: U,
}

impl<T: std::fmt::Debug, U: std::fmt::Debug> Par<T, U> {
    fn exibir(&self) {
        println!("Primeiro: {:?}, Segundo: {:?}", self.primeiro, self.segundo);
    }
}

fn main() {
    let par = Par {
        primeiro: 10,
        segundo: "texto",
    };
    par.exibir();
}

Aqui, o trait bound Debug é aplicado apenas aos métodos que precisam exibir os valores. Outras operações na struct podem não exigir essa restrição.

Trait Bounds e Implementações Especializadas

Restrições Avançadas

Rust permite definir implementações especializadas para casos específicos. Isso é chamado de specialization (ainda em fase experimental) ou uso estratégico de trait bounds:

struct Conteiner<T> {
    items: Vec<T>,
}

// Implementação geral
impl<T> Conteiner<T> {
    fn novo() -> Self {
        Conteiner { items: Vec::new() }
    }

    fn adicionar(&mut self, item: T) {
        self.items.push(item);
    }
}

// Implementação especializada para tipos Clone
impl<T: Clone> Conteiner<T> {
    fn duplicar_primeiro(&mut self) {
        if let Some(primeiro) = self.items.first() {
            self.items.push(primeiro.clone());
        }
    }
}

fn main() {
    let mut cont = Conteiner::novo();
    cont.adicionar(5);
    cont.adicionar(10);
    cont.duplicar_primeiro();

    println!("{:?}", cont.items); // [5, 10, 5]
}

A implementação de duplicar_primeiro() está disponível apenas se T implementa Clone. Isso oferece flexibilidade sem comprometer a segurança.

Where Clauses

Para trait bounds complexos, a syntax where melhora a legibilidade:

fn processar_pares<T, U>(a: T, b: U)
where
    T: std::fmt::Display + std::cmp::PartialOrd<U>,
    U: std::fmt::Display,
{
    println!("Processando: {} e {}", a, b);
}

A cláusula where deixa claro quais restrições se aplicam a cada tipo, especialmente útil em assinaturas complexas.

Conclusão

Generics em Rust permitem escrever código altamente reutilizável mantendo segurança de tipos total. Os três pontos-chave são: (1) funções genéricas reduzem duplicação parametrizando operações comuns; (2) structs genéricas criam contêineres flexíveis que funcionam com qualquer tipo, da mesma forma que Vec<T> e Option<T>; (3) trait bounds garantem que operações específicas estejam disponíveis, evitando erros em tempo de compilação. Dominando generics, você terá as ferramentas para escrever bibliotecas robustas e extensíveis.

Referências


Artigos relacionados