Genéricos
Nesta aula, exploramos o sistema de genéricos em Rust, que permite escrever código reutilizável para diferentes tipos. Abordamos funções genéricas, structs e enums genéricos, o processo de monomorfização e os limites (traits bounds) que restringem tipos permitidos. Ao final, você será capaz de criar abstrações seguras e eficientes.
Genéricos são um recurso fundamental em Rust que permite escrever código flexível e reutilizável sem sacrificar a segurança de tipos. Em vez de escrever funções ou estruturas separadas para cada tipo, você pode definir uma versão genérica que funciona com muitos tipos diferentes. O sistema de genéricos de Rust é poderoso e, combinado com traits, permite criar abstrações de alto desempenho graças à monomorfização em tempo de compilação.
Nesta aula, veremos como declarar e usar genéricos em funções, structs e enums, entenderemos o processo de monomorfização que gera código especializado para cada tipo concreto, e aprenderemos a impor restrições com limites (bounds) para garantir que os tipos genéricos tenham comportamentos específicos.
Funções genéricas
Uma função genérica é definida com um parâmetro de tipo entre colchetes angulares (<T>) antes da lista de parâmetros. Esse parâmetro pode ser usado no tipo dos argumentos e no tipo de retorno. Por exemplo, uma função que retorna o maior de dois valores pode ser genérica para qualquer tipo que suporte comparação:
fn maior<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", maior(10, 20)); // 20
println!("{}", maior(10.5, 2.3)); // 10.5
println!("{}", maior('a', 'z')); // 'z'
}
No exemplo, T é um tipo genérico que deve implementar o trait PartialOrd (para usar >). A função funciona com inteiros, floats e caracteres. O compilador gera uma versão especializada para cada tipo chamado em tempo de compilação.
Você pode ter múltiplos parâmetros de tipo, separados por vírgula:
fn converter<T, U>(valor: T) -> U
where
T: Into<U>,
{
valor.into()
}
Structs e enums genéricos
Assim como funções, structs e enums podem ser genéricos. Uma struct genérica tem um ou mais parâmetros de tipo que podem ser usados em seus campos:
struct Ponto<T> {
x: T,
y: T,
}
fn main() {
let inteiro = Ponto { x: 5, y: 10 };
let flutuante = Ponto { x: 1.0, y: 4.0 };
}
Também é possível ter diferentes tipos para cada campo:
struct Par<T, U> {
primeiro: T,
segundo: U,
}
let par = Par { primeiro: "Olá", segundo: 42 };
Enums genéricos funcionam de forma análoga. O exemplo clássico é Option<T> e Result<T, E>:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Esses enums são amplamente utilizados na biblioteca padrão para lidar com valores opcionais e tratamento de erros.
Monomorfização
Monomorfização é o processo pelo qual o compilador Rust substitui cada parâmetro de tipo genérico pelo tipo concreto usado em cada chamada, gerando código específico para cada combinação. Isso elimina a sobrecarga de despacho dinâmico, resultando em código tão eficiente quanto se você tivesse escrito versões separadas manualmente.
Por exemplo, considere a função genérica maior definida anteriormente. Se você chamá-la com i32 e depois com f64, o compilador gera duas funções: uma para i32 e outra para f64. O código resultante é equivalente a:
fn maior_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn maior_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}
Essa especialização acontece em tempo de compilação, sem custo em tempo de execução. A desvantagem é o aumento no tamanho do binário (bloat) se muitas instanciações forem geradas, mas na prática isso é gerenciável.
Limites
Limites (bounds) são restrições impostas aos parâmetros de tipo usando traits. Eles garantem que o tipo genérico suporte as operações necessárias. A sintaxe mais comum é T: Trait ou a cláusula where para maior clareza quando há múltiplos limites.
fn imprimir<T: std::fmt::Display>(valor: T) {
println!("{}", valor);
}
fn imprimir_where<T>(valor: T)
where
T: std::fmt::Display,
{
println!("{}", valor);
}
Você pode combinar múltiplos limites com +:
fn clonar_e_imprimir<T: Clone + std::fmt::Display>(valor: T) {
let copia = valor.clone();
println!("{}", copia);
}
Limites também podem ser usados em structs e enums para restringir implementações de métodos:
struct Par<T> {
primeiro: T,
segundo: T,
}
impl<T: PartialOrd> Par<T> {
fn maior(&self) -> &T {
if self.primeiro > self.segundo {
&self.primeiro
} else {
&self.segundo
}
}
}
Nesse exemplo, o método maior só está disponível quando T implementa PartialOrd.
Boas práticas
Ao usar genéricos, prefira limites específicos em vez de Any ou sem limites, para garantir que o código seja seguro e expressivo. Use a cláusula where para melhorar a legibilidade quando houver muitos limites. Evite genéricos desnecessários; se um tipo é sempre o mesmo, não o torne genérico. Lembre-se de que a monomorfização pode aumentar o tamanho do binário, mas geralmente é um bom custo-benefício para desempenho.
Referências
- The Rust Programming Language: Generic Data Types
- Rust by Example: Generics
- Rust Reference: Generics
- Trait std::convert::Into
- Trait std::cmp::PartialOrd
- Trait std::fmt::Display
- Trait std::clone::Clone
Exercícios
-
Escreva uma função genérica chamada
menorque retorna o menor de dois valores. Use um limite apropriado.✓ Resposta:fn menor<T: PartialOrd>(a: T, b: T) -> T { if a < b { a } else { b } } -
Crie uma struct genérica
Caixaque armazena um único valor de qualquer tipo. Implemente um métodoobterque retorna uma referência ao valor interno.✓ Resposta:struct Caixa<T> { valor: T, } impl<T> Caixa<T> { fn obter(&self) -> &T { &self.valor } } fn main() { let c = Caixa { valor: 42 }; println!("{}", c.obter()); } -
Defina um enum
Resultado<T, E>com variantesSucesso(T)eFalha(E). Escreva uma função que aceita umResultadoe imprime uma mensagem para cada caso (usematch).✓ Resposta:enum Resultado<T, E> { Sucesso(T), Falha(E), } fn processar<T: std::fmt::Display, E: std::fmt::Display>(res: Resultado<T, E>) { match res { Resultado::Sucesso(valor) => println!("Sucesso: {}", valor), Resultado::Falha(erro) => println!("Falha: {}", erro), } } fn main() { processar(Resultado::Sucesso(10)); processar(Resultado::Falha("erro")); } -
Explique o que é monomorfização e cite uma vantagem e uma desvantagem.
✓ Resposta:Monomorfização é o processo de substituir parâmetros de tipo genérico por tipos concretos em tempo de compilação, gerando código especializado para cada combinação. Vantagem: desempenho máximo, sem custo de despacho dinâmico. Desvantagem: pode aumentar o tamanho do binário (bloat) se muitas instanciações forem geradas.
-
Escreva uma função genérica
somarque aceita dois valores do mesmo tipo e retorna sua soma. Dica: use o traitstd::ops::Add.✓ Resposta:use std::ops::Add; fn somar<T: Add<Output = T>>(a: T, b: T) -> T { a + b } fn main() { println!("{}", somar(2, 3)); // 5 println!("{}", somar(1.5, 2.5)); // 4.0 }