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
- Definição e chamada da função genérica
fn foo<T>(x: T) - Como usar a estrutura genérica
struct Point<T> - Uma compreensão aprofundada das enumerações genéricas
Option<T>eResult<T, E> - Como escrever um método genérico
impl<T> - Monomorfização — um mecanismo de expansão em tempo de compilação
- Uso de vários parâmetros de tipo
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:
graph LR
A["Generic Functions<br/>fn identity<T>(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("hello".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:
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
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
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 ⭐)
// ============================================
// 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:
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 automaticamenteTcom base no tipo real do argumento.
▶ Exemplo 2: Estrutura genérica — Sistema de coordenadas de pontos (Dificuldade ⭐⭐)
// ============================================
// 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:
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;xeysão do mesmo tipo (ambosT).impl<T> Point<T>implementa métodos comuns para todas as instâncias deT.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 ⭐⭐)
// ============================================
// 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:
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 tipoT(com ou sem valor), enquantoResult<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 ⭐⭐⭐)
// ============================================
// 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:
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áusulawhereé 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, comoPair<String, i32>ePair<&str, f64>.
▶ Exemplo 5: Exercício abrangente — Contêineres genéricos e algoritmos (Dificuldade ⭐⭐⭐)
// ============================================
// 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:
=== 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çãoimpl<T: Display>garante que o métodoprint_allesteja disponível apenas quando o tipo implementar Display; use a restriçãoPartialOrdpara 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), enquantodyn Anydetermina 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 Traitem 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 tipoSelf, ao passo que funções genéricas não podem.
P: Qual é a diferença entre os blocos
impl<T>eimpl? R:impl<T>é o método de implementação para todos os tipos T, enquanto o padrãoimplé 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. Oimpl Point<f64> { fn distance(&self) }está disponível apenas para oPoint<f64>.
P: Qual é a diferença entre a cláusula
wherepara limites de características e escrevê-los diretamente entre colchetes angulares? R: Ambas têm a mesma finalidade, mas a cláusulawhereoferece melhor legibilidade para restrições complexas.fn foo<T: Display + Clone, U: Debug>(t: T, u: U)é equivalente afn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debug. A cláusulawhereé recomendada quando há múltiplas restrições.
📖 Resumo
- Funções genéricas
fn foo<T>(x: T)permitem que a mesma função seja aplicada a vários tipos - Estruturas genéricas
struct Point<T>permitem que os campos de estrutura armazenem qualquer tipo - Enumerações genéricas
Option<T>eResult<T, E>são exemplos clássicos de design genérico na biblioteca padrão do Rust. - Métodos genéricos
impl<T> Type<T>implementam métodos para todos os parâmetros de tipo e também podem implementar métodos específicos para tipos específicos - Monomorfização é o processo pelo qual um compilador expande código genérico em código para tipos específicos em tempo de compilação — sem nenhuma sobrecarga em tempo de execução.
- As cláusulas de restrição com múltiplos parâmetros de tipo
<T, U>ewheretornam os genéricos flexíveis e seguros
📝 Exercícios
- Dificuldade ⭐: Escreva uma função genérica
fn echo<T>(x: T) -> Tque receba um valor e o retorne tal como está. Na funçãomain, chame-a comi32,f64e&str, respectivamente. - Dificuldade ⭐⭐: Defina uma estrutura genérica
Container<T>que possua um campovalue: T. A implementaçãofn get(&self) -> &Tretorna uma referência ao valor efn set(&mut self, val: T)modifica o valor. Teste-a na função principal usandoContainer<String>eContainer<i32>, respectivamente. - 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 implementarClone. Em seguida, na função principal, una duas fatiasi32e duas fatias&str, respectivamente.