Genéricos
Esta aula aborda o sistema de genéricos em Rust, incluindo funções, structs e enums genéricos, o processo de monomorfização e o uso de limites (trait bounds) para restringir tipos. O conteúdo é progressivo, com exemplos práticos e exercícios para fixação.
Genéricos são uma ferramenta poderosa em Rust que permite escrever código flexível e reutilizável sem sacrificar a segurança de tipos. Eles permitem que funções, structs e enums operem sobre múltiplos tipos concretos, enquanto o compilador garante a correção em tempo de compilação. Esta aula explora os conceitos fundamentais de genéricos em Rust, desde a sintaxe básica até tópicos avançados como monomorfização e limites.
Dominar genéricos é essencial para criar bibliotecas e aplicações robustas em Rust, pois eles são a base para abstrações como traits, iterators e closures. Vamos começar com funções genéricas e depois expandir para tipos genéricos e restrições.
Funções genéricas
Funções genéricas permitem que uma única função trabalhe com diferentes tipos de dados. A sintaxe usa <T> antes dos parâmetros para declarar um parâmetro de tipo. Por exemplo, uma função que encontra o maior valor em um slice pode ser escrita genericamente:
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![3, 5, 1, 8, 2];
println!("Maior número: {}", maior(&numeros));
let chars = vec!['a', 'z', 'm', 'b'];
println!("Maior char: {}", maior(&chars));
}Neste exemplo, T é um parâmetro de tipo que deve implementar a trait PartialOrd (para comparação). O compilador irá gerar código específico para cada tipo concreto usado (monomorfização).
Structs e enums genéricos
Structs e enums também podem ser genéricos, permitindo que seus campos ou variantes armazenem valores de tipos arbitrários. Por exemplo, uma struct Ponto que representa coordenadas pode ser genérica sobre o tipo dos eixos:
struct Ponto<T> {
x: T,
y: T,
}
fn main() {
let ponto_int = Ponto { x: 5, y: 10 };
let ponto_float = Ponto { x: 1.5, y: 3.2 };
println!("Ponto int: ({}, {})", ponto_int.x, ponto_int.y);
println!("Ponto float: ({}, {})", ponto_float.x, ponto_float.y);
}Enums também podem ser genéricos, como o famoso Option<T> e Result<T, E> da biblioteca padrão. Você pode definir seus próprios enums genéricos:
enum Resultado<T, E> {
Sucesso(T),
Erro(E),
}
fn main() {
let sucesso: Resultado<i32, String> = Resultado::Sucesso(42);
let erro: Resultado<i32, String> = Resultado::Erro("Algo deu errado".to_string());
}Monomorfização
Monomorfização é o processo pelo qual o compilador Rust gera código específico para cada tipo concreto usado com um genérico. Isso significa que não há custo de desempenho em tempo de execução para usar genéricos; o código resultante é tão eficiente quanto se você tivesse escrito funções separadas para cada tipo. Por exemplo, a função maior acima, quando chamada com &[i32] e &[char], gera duas versões distintas da função, otimizadas para cada tipo.
A monomorfização é uma das razões pelas quais Rust é conhecido por oferecer abstrações de custo zero. No entanto, ela pode aumentar o tamanho do binário final, pois cada combinação de tipos gera seu próprio código. Em geral, isso é aceitável, mas em sistemas com restrições de espaço, pode ser necessário considerar alternativas como trait objects (dynamic dispatch).
Limites
Limites (trait bounds) são usados para restringir quais tipos podem ser usados com um genérico. Eles especificam que o tipo deve implementar uma ou mais traits. Por exemplo, a função maior exige PartialOrd. Você pode especificar múltiplos limites usando o sinal + ou a sintaxe where:
fn imprimir_e_maior<T: std::fmt::Display + PartialOrd>(a: T, b: T) {
println!("Comparando {} e {}", a, b);
if a > b {
println!("{} é maior", a);
} else {
println!("{} é maior", b);
}
}
// Alternativa com where
fn imprimir_e_maior<T>(a: T, b: T)
where
T: std::fmt::Display + PartialOrd,
{
println!("Comparando {} e {}", a, b);
if a > b {
println!("{} é maior", a);
} else {
println!("{} é maior", b);
}
}Limites também podem ser aplicados a structs e enums, mas note que a declaração do limite no struct não é herdada automaticamente pelos métodos; você precisa repetir o limite nos métodos que precisam dele:
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
}
}
}Boas práticas
Ao usar genéricos, prefira limites claros e específicos para evitar restrições desnecessárias. Use a sintaxe where para limites complexos, pois ela melhora a legibilidade. Lembre-se de que genéricos são resolvidos em tempo de compilação, então não há custo em tempo de execução. No entanto, evite usar genéricos quando uma trait object é mais apropriada (por exemplo, quando você precisa de heterogeneidade em uma coleção).
Referências
- The Rust Programming Language: Generic Data Types
- Rust by Example: Generics
- The Rust Reference: Generics
- Traits: Defining Shared Behavior
- Rust Standard Library: From trait (exemplo de trait usada em genéricos)
Exercícios
Escreva uma função genérica chamada
menorque recebe dois valores do mesmo tipo (que implementam PartialOrd) e retorna o menor deles.✓ Resposta:fn menor<T: PartialOrd>(a: T, b: T) -> T { if a < b { a } else { b } } fn main() { println!("{}", menor(3, 5)); // 3 println!("{}", menor('a', 'z')); // 'a' }Crie uma struct
Caixagenérica que armazena um valor de tipoT. Implemente um métodoobterque retorna uma referência ao valor.✓ Resposta:struct Caixa<T> { valor: T, } impl<T> Caixa<T> { fn obter(&self) -> &T { &self.valor } } fn main() { let caixa = Caixa { valor: 42 }; println!("{}", caixa.obter()); }Defina um enum
Opcaogenérico que possa representar um valor presente (Algum(T)) ou ausente (Nenhum). Em seguida, implemente uma função que retorna umOpcaoa partir de um índice em um vetor.✓ Resposta:enum Opcao<T> { Algum(T), Nenhum, } fn obter_elemento<T: Clone>(v: &[T], indice: usize) -> Opcao<T> { if indice < v.len() { Opcao::Algum(v[indice].clone()) } else { Opcao::Nenhum } } fn main() { let v = vec![10, 20, 30]; match obter_elemento(&v, 1) { Opcao::Algum(val) => println!("Valor: {}", val), Opcao::Nenhum => println!("Índice inválido"), } }Explique o que é monomorfização e por que ela é benéfica para o desempenho em Rust. Dê um exemplo conceitual.
✓ Resposta: Monomorfização é o processo de geração de código específico para cada tipo concreto usado com um genérico em tempo de compilação. Isso elimina a necessidade de dispatch dinâmico, resultando em código tão eficiente quanto se você tivesse escrito funções separadas para cada tipo. Por exemplo, se você usaVec<i32>eVec<String>, o compilador gera duas versões deVece seus métodos, otimizadas para cada tipo.Escreva uma função genérica
somarque aceita dois valores do mesmo tipo e retorna sua soma. O tipo deve implementar a traitstd::ops::Add. Use a sintaxewherepara o limite.✓ Resposta:use std::ops::Add; fn somar<T>(a: T, b: T) -> T where T: Add<Output = T>, { a + b } fn main() { println!("{}", somar(2, 3)); // 5 println!("{}", somar(1.5, 2.5)); // 4.0 }