Rust: Genéricos em Rust: Programação parametrizada por tipos

Última atualização: 2026-08-26

Genéricos são “programação parametrizada no nível do tipo” — a passagem de tipos como parâmetros para que o mesmo código possa ser aplicado a vários tipos sem a necessidade de reescrevê-lo para cada um deles.

Se uma função está “abstraindo valores em parâmetros”, então os genéricos também estão “abstraindo tipos em parâmetros”. É como quando você pede comida para viagem e diz: “Quero uma porção de arroz”, sem especificar se quer arroz frito ou arroz acompanhado de algum prato — você decide isso quando chega ao restaurante.


1. O que você vai aprender


2. Diagramas conceituais

O diagrama Mermaid a seguir ilustra o mecanismo pelo qual o parâmetro de tipo genérico T é substituído por um tipo concreto durante o processo de monomorfização em tempo de compilação:

100%
graph LR
    A["Generic Functions<br/>fn identity&lt;T&gt;(x: T) -> T"] --> B["Compile-Time Singletonization<br/>Monomorphization"]
    B --> C["T → i32<br/>fn identity_i32(x: i32) -> i32"]
    B --> D["T → String<br/>fn identity_string(x: String) -> String"]
    B --> E["T → f64<br/>fn identity_f64(x: f64) -> f64"]
    C --> F["Call identity(42)"]
    D --> G["Call identity(&#34;hello&#34;.to_string())"]
    E --> H["Call identity(3.14)"]

3. A história de um contêiner versátil

(1) O problema: escrever código repetitivo para cada tipo

Luna (Luna) está desenvolvendo um conjunto de ferramentas e precisa de uma função para “encontrar o valor máximo”.

No início, ela escreveu um para cada tipo:

RUST
fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}

fn max_str(a: &str, b: &str) -> &str {
    if a > b { a } else { b }
}

fn main() {
    println!("{}", max_i32(3, 7));     // 7
    println!("{}", max_f64(2.5, 1.8)); // 2.5
    println!("{}", max_str("apple", "banana")); // banana
}

Além dos diferentes tipos, a lógica dessas três funções é exatamente a mesma. Se também houvesse u32, u64, char e assim por diante, seria necessário copiar o código para cada tipo adicional. Isso é o que se chama de “programação do tipo copiar e colar” — nada elegante nem fácil de manter.

(2) A abordagem do Rust em relação aos genéricos

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

fn main() {
    println!("{}", max(3, 7));              // 7
    println!("{}", max(2.5, 1.8));          // 2.5
    println!("{}", max("apple", "banana")); // banana
}

Uma função genérica substitui três funções para tipos específicos — e o compilador gera automaticamente código especializado para cada tipo que for realmente utilizado (monomorfismo). Você escreve uma, e o compilador a expande em várias para você.


4. Conceitos fundamentais

(1) Sistema genérico

100%
graph TB
    A[Generics Generics] --> B[Generic Functions]
    A --> C[Generic Structures]
    A --> D[Generic Enumerations]
    A --> E[Generic Methods]

    B --> F["fn identity<T>(x: T) -> T"]
    C --> G["struct Point<T> { x: T, y: T }"]
    D --> H["Option<T>, Result<T, E>"]
    E --> I["impl<T> Point<T> { fn x(&self) -> &T }"]

    A --> J[Singleton Monomorphization]
    J --> K["At compile time:T → i32, f64, String ..."]
    J --> L["Generate separate code for each type"]

(2) Tipos genéricos x tipos concretos x despacho dinâmico

Característica Tipo específico (não genérico) Genérico (monomórfico) Despacho dinâmico (dyn Trait)
Duplicação de código Escrever uma cópia para cada tipo Expansão automática pelo compilador Uma única cópia do código, distribuída em tempo de execução
Desempenho Melhor Melhor (sem sobrecarga de funções virtuais) Com sobrecarga de funções virtuais
Tempo de compilação Longo (muito código escrito manualmente) Bastante longo (muitas expansões) Curto
Volume binário Grande Médio (uma cópia por tipo) Pequeno
Flexibilidade Baixa Determinada em tempo de compilação Determinada em tempo de execução

(3) Enumerações genéricas comuns

Enumeração Definição Finalidade
Option<T> enum Option<T> { Some(T), None } Valores que podem estar em branco
Result<T, E> enum Result<T, E> { Ok(T), Err(E) } Operações que podem falhar
Vec<T> struct Vec<T> { ... } Matrizes dinâmicas
HashMap<K, V> struct HashMap<K, V> { ... } Mapeamento chave-valor

(4) Comparação de métodos de restrição genéricos

Tipo de restrição Sintaxe Casos de uso Exemplo
Restrições em linha fn foo<T: Trait>(x: T) Restrições simples e únicas fn max<T: PartialOrd>(a: T, b: T)
Restrições múltiplas + fn foo<T: Trait1 + Trait2>(x: T) Restrições múltiplas fn print<T: Display + Clone>(x: T)
Cláusula WHERE fn foo<T>(x: T) where T: Trait Restrições complexas, parâmetros multitipo where T: Display + Clone, U: Debug
Traço impl fn foo(x: impl Trait) Notação abreviada (sintaxe simplificada) fn plug(d: &impl USBDevice)

5. Exemplos genéricos

▶ Exemplo 1: Função genérica — Encontrar o valor máximo em um array (Dificuldade ⭐)

RUST
// ============================================
// Generic Functions:Applies to any comparable type
// ============================================

// PartialOrd Constraint Assurance T Supports comparison operations
fn find_max<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list.iter() {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    let numbers = vec![3, 7, 1, 9, 4];
    println!("i32 Maximum value: {}", find_max(&numbers));

    let floats = vec![2.5, 1.8, 3.14, 0.99];
    println!("f64 Maximum value: {}", find_max(&floats));

    let strings = vec!["apple", "banana", "cherry", "date"];
    println!("&str Maximum value: {}", find_max(&strings));

    // The Same Function,Three Types,Compiler Auto-Expansion
}

Resultado:

TEXT 📖 Somente leitura
i32 Maximum value: 9
f64 Maximum value: 3.14
&str Maximum value: date

find_max<T> é uma função genérica, T é um parâmetro de tipo e <T: std::cmp::PartialOrd> é uma restrição de trait — o que significa que “T deve ser um tipo comparável”. Quando a função é chamada, o compilador infere automaticamente T com base no tipo real do argumento.


▶ Exemplo 2: Estrutura genérica — Sistema de coordenadas de pontos (Dificuldade ⭐⭐)

RUST
// ============================================
// Generic Structures:Point Can store coordinates of any type
// ============================================

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

// Implementation Methods for Generic Structures
impl<T> Point<T> {
    // Back x Citation
    fn x(&self) -> &T {
        &self.x
    }

    // Back y Citation
    fn y(&self) -> &T {
        &self.y
    }
}

// For Point of a specific type f64, implement additional methods
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let int_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 3.0, y: 4.0 };
    let string_point = Point {
        x: "left",
        y: "right",
    };

    println!("int_point: {:?}", int_point);
    println!("float_point x: {}", float_point.x());
    println!("string_point: {:?}", string_point);

    // Only `Point<f64>` has the distance_from_origin method
    println!("Distance from the origin: {:.2}", float_point.distance_from_origin());

    // Compilation Error: int_point is Point<i32>, no such method
    // println!("{}", int_point.distance_from_origin());
}

Resultado:

TEXT 📖 Somente leitura
int_point: Point { x: 5, y: 10 }
float_point x: 3.0
string_point: Point { x: "left", y: "right" }
Distance from the origin: 5.00

Point<T> é uma estrutura genérica; x e y são do mesmo tipo (ambos T). impl<T> Point<T> implementa métodos comuns para todas as instâncias de T. impl Point<f64> implementa métodos específicos apenas para determinados tipos — essa é uma das principais vantagens dos genéricos.


▶ Exemplo 3: Enumerações genéricas — Option e Result na prática (Dificuldade ⭐⭐)

RUST
// ============================================
// Generic Enumerations: Option<T> and Result<T, E> Usage
// ============================================

// Custom Result Style Enumeration
#[derive(Debug)]
enum MyResult<T, E> {
    Success(T),
    Failure(E),
}

// Division Function:Back Result Style
fn safe_divide<T>(a: T, b: T) -> MyResult<T, String>
where
    T: std::ops::Div<Output = T> + std::cmp::PartialEq + From<u8> + Copy,
{
    if b == 0.into() {
        MyResult::Failure("Division by zero".to_string())
    } else {
        MyResult::Success(a / b)
    }
}

// Using the standard library Option<T>
fn find_in_vector<T: PartialEq>(vec: &[T], target: &T) -> Option<usize> {
    for (i, item) in vec.iter().enumerate() {
        if item == target {
            return Some(i);
        }
    }
    None
}

// Using the standard library Result<T, E>
fn parse_number(s: &str) -> Result<i32, String> {
    s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}

fn main() {
    // Option Usage
    let numbers = vec![10, 20, 30, 40, 50];
    match find_in_vector(&numbers, &30) {
        Some(index) => println!("Found 30,Index: {}", index),
        None => println!("Not found"),
    }
    match find_in_vector(&numbers, &99) {
        Some(index) => println!("Found 99,Index: {}", index),
        None => println!("Not found 99"),
    }

    // Result Usage
    match parse_number("42") {
        Ok(n) => println!("Parsing Successful: {}", n),
        Err(e) => println!("Parsing Failed: {}", e),
    }
    match parse_number("hello") {
        Ok(n) => println!("Parsing Successful: {}", n),
        Err(e) => println!("Parsing Failed: {}", e),
    }

    // Custom MyResult Usage
    let result = safe_divide(10.0, 3.0);
    println!("Custom Result: {:?}", result);

    let result = safe_divide(10.0, 0.0);
    println!("Custom Result: {:?}", result);
}

Resultado:

TEXT 📖 Somente leitura
Found 30,Index: 2
Not found 99
Parsing Successful: 42
Parsing Failed: Parse error: invalid digit found in string
Custom Result: Success(3.3333333333333335)
Custom Result: Failure("Division by zero")

Option<T> possui apenas um parâmetro de tipo T (com ou sem valor), enquanto Result<T, E> possui dois parâmetros de tipo (o tipo de sucesso e o tipo de erro). As enums genéricas permitem que esses tipos sejam aplicados a qualquer tipo de dados — esse é um dos princípios fundamentais de design da biblioteca padrão do Rust.


▶ Exemplo 4: Vários parâmetros de tipo e monomorfismo (Dificuldade ⭐⭐⭐)

RUST
// ============================================
// Multiple Type Parameters + Combining Generic Methods
// ============================================

use std::fmt::Display;

// Generic struct with two type parameters
#[derive(Debug)]
struct Pair<K, V> {
    key: K,
    value: V,
}

// Implement methods for Pair<K, V>
impl<K, V> Pair<K, V> {
    fn new(key: K, value: V) -> Self {
        Pair { key, value }
    }
}

// Constrained methods: only available when both K and V implement Display
impl<K: Display, V: Display> Pair<K, V> {
    fn print(&self) {
        println!("Key: {}, Value: {}", self.key, self.value);
    }
}

// Generic Methods:Mixing Different Types of Parameters
fn mix_and_match<T, U>(a: T, b: U) -> String
where
    T: Display,
    U: Display,
{
    format!("Mixed: {} and {}", a, b)
}

fn main() {
    // Multiple Type Parameters: String and i32
    let pair1 = Pair::new("Age".to_string(), 25);
    pair1.print();

    // Multiple Type Parameters: &str and f64
    let pair2 = Pair::new("PI", 3.14159);
    pair2.print();

    // Different Types of Combinations
    let pair3 = Pair::new(100, "HTTP OK");
    // pair3.print();  // ❌ Compilation Error: i32 and &str both implement Display, but no error here.
    // In fact i32 and &str both implement Display, so you can call it
    // This is just to demonstrate the concept of constraint methods.
    pair3.print();

    // Mixing Different Types
    println!("{}", mix_and_match(42, "answer"));
    println!("{}", mix_and_match(3.14, 100));
}

Resultado:

TEXT 📖 Somente leitura
Key: Age, Value: 25
Key: PI, Value: 3.14159
Key: 100, Value: HTTP OK
Mixed: 42 and answer
Mixed: 3.14 and 100

Parâmetros de tipo múltiplos (<K, V>) permitem que uma estrutura armazene dados de diferentes tipos. A cláusula where é usada para restringir as condições que os parâmetros de tipo devem satisfazer. Durante a monomorfização, o compilador gera código separado para cada combinação, como Pair<String, i32> e Pair<&str, f64>.


▶ Exemplo 5: Exercício abrangente — Contêineres genéricos e algoritmos (Dificuldade ⭐⭐⭐)

RUST
// ============================================
// Comprehensive Example:Generic Stack + Generic Search Algorithms
// ============================================

use std::fmt::Display;

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

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

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

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn peek(&self) -> Option<&T> {
        self.items.last()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    fn len(&self) -> usize {
        self.items.len()
    }
}

impl<T: Display> Stack<T> {
    fn print_all(&self) {
        for item in &self.items {
            print!("{} ", item);
        }
        println!();
    }
}

fn find_first<T: PartialEq>(items: &[T], target: &T) -> Option<usize> {
    items.iter().position(|x| x == target)
}

fn swap_if_greater<T: PartialOrd>(a: &mut T, b: &mut T) {
    if *a > *b {
        std::mem::swap(a, b);
    }
}

fn main() {
    let mut int_stack: Stack<i32> = Stack::new();
    int_stack.push(10);
    int_stack.push(20);
    int_stack.push(30);
    println!("=== Integer Stack ===");
    println!("Stack Contents: ");
    int_stack.print_all();
    println!("Stack top: {:?}", int_stack.peek());
    println!("Pop up: {:?}", int_stack.pop());
    println!("Remaining {} element", int_stack.len());

    let mut str_stack: Stack<&str> = Stack::new();
    str_stack.push("Rust");
    str_stack.push("is");
    str_stack.push("awesome");
    println!("\n=== String Stack ===");
    str_stack.print_all();

    let nums = vec![10, 20, 30, 40, 50];
    println!("\n=== Generic Search ===");
    println!("Find 30: Index {:?}", find_first(&nums, &30));
    println!("Find 99: Index {:?}", find_first(&nums, &99));

    let words = vec!["apple", "banana", "cherry"];
    println!("Find 'banana': Index {:?}", find_first(&words, &"banana"));

    let mut x = 42;
    let mut y = 10;
    println!("\n=== Generic Swaps ===");
    println!("Before the exchange: x={}, y={}", x, y);
    swap_if_greater(&mut x, &mut y);
    println!("After the exchange: x={}, y={}", x, y);
}

Resultado:

TEXT 📖 Somente leitura
=== Integer Stack ===
Stack Contents: 
10 20 30 
Stack top: Some(30)
Pop up: Some(30)
Remaining 2 element

=== String Stack ===
Rust is awesome 

=== Generic Search ===
Find 30: Index Some(2)
Find 99: Index None
Find 'banana': Index Some(1)

=== Generic Swaps ===
Before the exchange: x=42, y=10
After the exchange: x=10, y=42

O tipo genérico Stack<T> funciona tanto para i32 quanto para &str; a restrição impl<T: Display> garante que o método print_all esteja disponível apenas quando o tipo implementar Display; use a restrição PartialOrd para implementar o operador de comparação genérico.


❓ Perguntas Frequentes

P: Qual é a diferença entre genéricos e Box<dyn Any>? R: Os genéricos determinam os tipos em tempo de compilação (despacho estático), enquanto dyn Any determina os tipos em tempo de execução (despacho dinâmico). Os genéricos oferecem melhor desempenho porque o compilador gera código especializado para cada tipo, eliminando a sobrecarga das funções virtuais. dyn Any é mais flexível, pois pode lidar com qualquer tipo em tempo de execução, mas acarreta sobrecarga de tempo de execução.

P: A monomorfização resulta em arquivos binários maiores? R: Sim, mas o impacto costuma ser mínimo. É gerado um arquivo de código separado para cada combinação de tipos efetivamente utilizada. Se uma função genérica for chamada com um grande número de tipos diferentes, o tamanho do binário aumentará. No entanto, o compilador do Rust realiza otimizações, e o “tamanho do código” geralmente não é um gargalo nas CPUs modernas. Se o tamanho for uma preocupação, considere usar dyn Trait em vez disso.

P: Qual é a diferença entre funções genéricas e métodos genéricos? R: Funções genéricas são funções independentes, enquanto métodos genéricos são funções definidas em tipos. Funções genéricas: fn foo<T>(x: T). Métodos genéricos: impl<T> MyType<T> { fn bar(&self) }. Métodos genéricos podem acessar o tipo Self, ao passo que funções genéricas não podem.

P: Qual é a diferença entre os blocos impl<T> e impl? R: impl<T> é o método de implementação para todos os tipos T, enquanto o padrão impl é o método de implementação para tipos específicos. impl<T> Point<T> { fn x(&self) } está disponível para todos os tipos Point. O impl Point<f64> { fn distance(&self) } está disponível apenas para o Point<f64>.

P: Qual é a diferença entre a cláusula where para limites de características e escrevê-los diretamente entre colchetes angulares? R: Ambas têm a mesma finalidade, mas a cláusula where oferece melhor legibilidade para restrições complexas. fn foo<T: Display + Clone, U: Debug>(t: T, u: U) é equivalente a fn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debug. A cláusula where é recomendada quando há múltiplas restrições.


📖 Resumo


📝 Exercícios

  1. Dificuldade ⭐: Escreva uma função genérica fn echo<T>(x: T) -> T que receba um valor e o retorne tal como está. Na função main, chame-a com i32, f64 e &str, respectivamente.
  2. Dificuldade ⭐⭐: Defina uma estrutura genérica Container<T> que possua um campo value: T. A implementação fn get(&self) -> &T retorna uma referência ao valor e fn set(&mut self, val: T) modifica o valor. Teste-a na função principal usando Container<String> e Container<i32>, respectivamente.
  3. Dificuldade ⭐⭐⭐: Escreva uma função genérica fn merge_arrays<T>(a: &[T], b: &[T]) -> Vec<T> que una duas fatias e retorne um novo Vec. O tipo T deve implementar Clone. Em seguida, na função principal, una duas fatias i32 e duas fatias &str, respectivamente.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%