Genéricos avançado em Rust
LidoEsta aula explora recursos avançados de genéricos em Rust, incluindo tipos associados, genéricos const, PhantomData e uma visão geral sobre especialização, com exemplos práticos e exercícios para fixação.
Nesta aula, avançaremos no estudo dos genéricos em Rust, abordando conceitos que permitem criar abstrações mais poderosas e seguras. Você aprenderá a usar tipos associados para definir relações entre tipos, genéricos const para trabalhar com constantes de tempo de compilação, e PhantomData para controlar o comportamento do verificador de tipos. Também discutiremos a especialização, um recurso ainda instável que permite implementações mais específicas de traits.
Esses recursos são fundamentais para construir bibliotecas robustas, como coleções, iteradores e APIs que exigem alta expressividade sem sacrificar a performance ou a segurança.
Tipos associados
Tipos associados são uma forma de definir um tipo que pertence a um trait, permitindo que cada implementação do trait especifique qual tipo concreto será usado. Isso é útil quando um trait precisa referenciar um tipo que pode variar conforme a implementação, sem tornar o trait genérico em relação a esse tipo.
Um exemplo clássico é o trait Iterator, que define um tipo associado Item para representar o tipo dos elementos produzidos. Assim, cada implementação pode especificar seu próprio tipo de item, tornando o código mais limpo e legível.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Contador {
count: u32,
}
impl Iterator for Contador {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count <= 5 {
Some(self.count)
} else {
None
}
}
}Ao contrário de usar um parâmetro de tipo genérico no trait, os tipos associados garantem que uma implementação tenha apenas um tipo associado, evitando ambiguidades quando o trait é usado como limite (bound). Por exemplo, se um trait tivesse um parâmetro genérico T, uma mesma implementação poderia existir para múltiplos T, mas com tipos associados isso não acontece, simplificando a lógica de tipos.
Genéricos const
Genéricos const permitem que um item (como uma struct, enum ou função) seja genérico sobre valores constantes, como números inteiros ou booleanos, em vez de apenas tipos. Isso é útil para criar tipos que dependem de tamanhos ou configurações conhecidas em tempo de compilação, como arrays de tamanho fixo.
Antes de os genéricos const serem estabilizados, era necessário criar macros ou usar bibliotecas externas para simular esse comportamento. Agora, podemos escrever código como:
struct Array<T, const N: usize> {
data: [T; N],
}
impl<T: Copy, const N: usize> Array<T, N> {
fn new(value: T) -> Self {
Array { data: [value; N] }
}
}
fn main() {
let a: Array<i32, 3> = Array::new(10);
println!("Tamanho: {}", a.data.len());
}Genéricos const também podem ser usados em funções, permitindo criar algoritmos que dependem de constantes, como a exponenciação de inteiros em tempo de compilação:
fn potenciacao_const<const BASE: u32, const EXP: u32>() -> u32 {
let mut resultado = 1;
let mut i = 0;
while i < EXP {
resultado *= BASE;
i += 1;
}
resultado
}
fn main() {
let valor: u32 = potenciacao_const::<2, 10>();
println!("2^10 = {}", valor);
}É importante notar que os genéricos const têm restrições: apenas certos tipos podem ser usados como constantes (inteiros, chars, bools), e as operações devem ser possíveis em tempo de compilação. Isso garante que o código seja eficiente, pois muitas vezes pode ser avaliado durante a compilação.
PhantomData
PhantomData é um tipo especial usado para simular a posse de um tipo genérico sem realmente armazená-lo. Ele é útil para indicar ao verificador de tipos que um tipo está relacionado a outro, mesmo que não apareça nos campos da struct. Isso é comum em APIs que usam zero-sized types (ZSTs) ou para controlar a variância e o auto-trait behavior.
Um exemplo clássico é quando queremos criar um tipo que representa um ponteiro para um tipo T, mas sem armazenar o valor. Podemos usar PhantomData para que o compilador saiba que o tipo é covariante em T e que não pode ser enviado entre threads se T não for Send.
use std::marker::PhantomData;
struct Ponteiro<T> {
endereco: usize,
_marcador: PhantomData<T>,
}
impl<T> Ponteiro<T> {
fn novo(endereco: usize) -> Self {
Ponteiro {
endereco,
_marcador: PhantomData,
}
}
}
fn main() {
let ptr: Ponteiro<String> = Ponteiro::novo(0x1234);
// O compilador sabe que Ponteiro<String> não é Send se String não for Sync,
// graças ao PhantomData.
}PhantomData também é usado para implementar tipos como Rc ou Arc para rastrear a propriedade, ou em builders para garantir que certos métodos sejam chamados. Em resumo, PhantomData permite que o sistema de tipos tenha informações que não estão presentes fisicamente na estrutura, aumentando a segurança da API.
Especialização (visão geral)
Especialização é um recurso que permite que um trait tenha implementações mais específicas que se sobreponham a implementações genéricas. Em Rust, o compilador normalmente proíbe implementações sobrepostas para evitar ambiguidade, mas com especialização podemos definir uma implementação genérica e uma específica para um caso particular, desde que não haja conflito.
Esse recurso ainda é instável e está disponível apenas no canal nightly. Ele é útil para otimizar comportamentos, como fornecer uma implementação padrão para todos os tipos e uma implementação especial para um tipo específico que seja mais eficiente.
#![feature(specialization)]
trait Converter {
fn converter(&self) -> String;
}
// Implementação genérica
impl<T> Converter for T {
default fn converter(&self) -> String {
format!("{:?}", self)
}
}
// Implementação especializada para u32
impl Converter for u32 {
fn converter(&self) -> String {
format!("Número: {}", self)
}
}
fn main() {
let x = 42u32;
println!("{}", x.converter()); // Imprime "Número: 42"
}No exemplo, a implementação para u32 é mais específica que a genérica, então o compilador a escolhe. A palavra-chave default indica que a implementação pode ser sobrescrita por uma mais específica. Especialização é um tópico avançado e ainda em evolução; é importante acompanhar as RFCs e a documentação oficial para entender as limitações atuais.
Boas práticas e observações
Ao usar genéricos avançados, alguns cuidados são essenciais:
- Utilize tipos associados quando quiser que cada implementação defina um tipo específico, como em traits de coleções.
- Genéricos const são ótimos para dimensionar estruturas em tempo de compilação, mas evite usar constantes muito grandes que possam aumentar o tempo de compilação.
- PhantomData deve ser usado com moderação; ele informa ao compilador sobre relações de tipo, mas não adiciona nenhum comportamento em tempo de execução.
- Especialização é instável; prefira evitar em código de produção até que seja estabilizada, a menos que você esteja no nightly e entenda os riscos.
Entender esses conceitos eleva seu nível de Rust e permite criar abstrações mais elegantes e seguras.
Referências
- Livro de Rust - Traits Avançados
- Referência da Linguagem - Itens Associados
- Documentação do PhantomData
- Referência da Linguagem - Genéricos Const
- RFC 1210 - Especialização
- Blog Rust - Iteração de Genéricos Const
Exercícios
Implemente um trait
Areacom um tipo associadoOutpute calcule a área para um quadrado e um círculo. O tipo associado deve serf64para ambos.✓ Resposta:trait Area { type Output; fn area(&self) -> Self::Output; } struct Quadrado { lado: f64 } struct Circulo { raio: f64 } impl Area for Quadrado { type Output = f64; fn area(&self) -> Self::Output { self.lado * self.lado } } impl Area for Circulo { type Output = f64; fn area(&self) -> Self::Output { std::f64::consts::PI * self.raio * self.raio } }Crie uma função genérica
tamanho_arrayque retorna o tamanho de um array usando genéricos const. Teste com arrays de tamanhos diferentes.✓ Resposta:fn tamanho_array<T, const N: usize>(_arr: &[T; N]) -> usize { N } fn main() { let a = [1, 2, 3]; let b = [true, false]; println!("{}", tamanho_array(&a)); // 3 println!("{}", tamanho_array(&b)); // 2 }Explique por que precisamos de PhantomData em uma struct que contém um ponteiro para
Tsem armazenar um valor do tipoT. Dê um exemplo de uso.✓ Resposta: PhantomData é necessário para que o compilador saiba que a struct possui um tipoT, afetando a variância e os auto-traits (como Send e Sync). Sem ele, o compilador não saberia que a struct não é Send se T não for Send. Exemplo:struct Ponteiro<T> { endereco: usize, _marker: PhantomData<T> } fn main() { let p: Ponteiro<String> = Ponteiro { endereco: 0, _marker: PhantomData }; }Escreva um código que utilize especialização para ter uma implementação genérica de um trait e uma específica para um tipo particular. Lembre-se de que é necessário usar o canal nightly.
✓ Resposta:#![feature(specialization)] trait Descrever { fn descrever(&self) -> String; } impl<T> Descrever for T { default fn descrever(&self) -> String { format!("Genérico") } } impl Descrever for i32 { fn descrever(&self) -> String { format!("Inteiro: {}", self) } } fn main() { println!("{}", 5.descrever()); }Diferencie tipos associados de parâmetros de tipo genérico em traits. Dê um exemplo em que tipos associados são mais adequados.
✓ Resposta: Tipos associados são melhores quando cada implementação deve ter exatamente um tipo concreto, evitando múltiplas implementações para o mesmo tipo. Parâmetros de tipo permitem múltiplas implementações, o que pode causar ambiguidade. Exemplo: trait Conversor { type Saida; fn converter(&self) -> Self::Saida; } vs trait Conversor<T> { fn converter(&self) -> T; }.