Tipos avançados
LidoNesta aula, exploramos tipos avançados em Rust: o never type (!), tipos de tamanho dinâmico (DST), aliases de tipo e o uso de impl Trait em retornos. Cada conceito é explicado com exemplos práticos, boas práticas e exercícios para consolidar o aprendizado.
Nesta aula, vamos aprofundar nosso conhecimento em alguns tipos e construções avançadas da linguagem Rust. Estes recursos são fundamentais para escrever código mais expressivo, seguro e idiomático. Vamos explorar o never type (!), que representa a ausência de valor; os tipos de tamanho dinâmico (DST), que são essenciais para trabalhar com dados de tamanho desconhecido em tempo de compilação; os aliases de tipo, que melhoram a legibilidade; e o uso de impl Trait em retornos, que simplifica assinaturas de funções sem sacrificar a abstração.
Dominar esses conceitos é crucial para avançar em tópicos como programação assíncrona, geração de código e design de APIs. Cada um deles aparece em situações comuns, desde funções que nunca retornam até a criação de iteradores complexos. Ao final, você terá uma compreensão sólida e poderá aplicar esses tipos em seus próprios projetos.
Never type (!)
O never type, denotado por !, é um tipo especial que representa o resultado de computações que nunca produzem um valor. Em Rust, ele é usado para funções que entram em loop infinito, panics, ou que encerram o processo. Por exemplo, a função panic! tem tipo ! porque ela nunca retorna normalmente.
Uma propriedade interessante é que o tipo ! pode ser coagido para qualquer outro tipo. Isso permite que ele seja usado em posições onde um valor de outro tipo é esperado, como em um match ou em uma expressão if. Por exemplo, em um match, se um braço chama uma função que retorna !, esse braço pode ter qualquer tipo, pois a função nunca retorna.
fn divergir() -> ! {
panic!("Esta função nunca retorna");
}
fn main() {
let x: i32 = match 42 {
0 => 10,
_ => divergir(), // divergir() retorna !, mas pode ser usado como i32
};
println!("x = {}", x);
}
Outro uso comum é em loops infinitos, como em um servidor que fica escutando conexões. A função loop {} tem tipo ! porque nunca termina. O compilador reconhece isso e permite que a expressão seja usada em qualquer contexto que exija um valor.
fn main() -> ! {
loop {
// processa algo para sempre
}
}
É importante notar que o tipo ! é instável em algumas situações, mas é amplamente utilizado em código de baixo nível e em bibliotecas como std::process::exit.
Tipos de tamanho dinâmico (DST)
Em Rust, a maioria dos tipos tem tamanho conhecido em tempo de compilação. No entanto, existem tipos cujo tamanho só é conhecido em tempo de execução, chamados de DST (Dynamic Sized Types). O exemplo mais clássico é str (sem o prefixo & ou Box), que é uma sequência de bytes UTF-8 sem tamanho fixo. Outros exemplos incluem [T] (fatia sem tamanho) e dyn Trait (trait object).
DSTs não podem ser usados diretamente como valores; eles devem ser sempre usados atrás de um ponteiro, como &str, &[T], Box<str>, Box<[T]> ou Box<dyn Trait>. Isso ocorre porque o compilador precisa saber o tamanho para alocar memória, e DSTs não têm tamanho fixo. O ponteiro carrega informações adicionais, como o comprimento (para fatias) ou um ponteiro para a vtable (para trait objects).
// str é um DST
let s: &str = "Olá, mundo!";
// [i32] é um DST
let arr: &[i32] = &[1, 2, 3];
// dyn Display é um DST (trait object)
fn mostrar(x: &dyn std::fmt::Display) {
println!("{}", x);
}
Uma característica importante dos DSTs é que eles podem ser usados em tipos genéricos com a sintaxe especial ?Sized. Por padrão, os parâmetros de tipo são Sized, mas podemos permitir DSTs usando esse bound. Isso é útil em funções que aceitam fatias ou strings sem tamanho fixo.
fn imprimir_fatia<T: ?Sized>(fatia: &T) {
// ...
}
Além disso, é possível criar seus próprios DSTs, mas isso é raro e requer unsafe. Na prática, a maioria dos desenvolvedores interage com DSTs apenas através de referências e ponteiros inteligentes.
Aliases
Aliases de tipo permitem criar nomes alternativos para tipos existentes, melhorando a legibilidade e a manutenção do código. A sintaxe é type Nome = Tipo;. Eles não criam um novo tipo, apenas um sinônimo, então o compilador os trata como o mesmo tipo.
Um uso comum é para abreviar tipos complexos, como ponteiros para funções ou tipos genéricos. Por exemplo, em um sistema de processamento de eventos, podemos definir:
type Callback = Box<dyn Fn(i32) -> i32>;
fn registrar_callback(cb: Callback) {
// ...
}
Outro uso é para descrever a intenção do código. Por exemplo, em vez de usar u64 diretamente, podemos criar um alias type UserId = u64; para deixar claro o significado. Isso facilita mudanças futuras, pois se o tipo subjacente mudar, só precisamos alterar a definição do alias.
type UserId = u64;
type Matricula = u64;
fn buscar_usuario(id: UserId) {
// ...
}
Aliases também são úteis para reduzir a repetição de tipos longos em assinaturas de funções, especialmente em código com muitos genéricos. No entanto, é importante não exagerar, pois aliases demais podem tornar o código confuso. Use-os com moderação e sempre com nomes descritivos.
impl Trait em retornos
O recurso impl Trait em posição de retorno permite que uma função retorne um tipo que implementa um trait específico, sem precisar nomear o tipo concreto. Isso é útil para ocultar o tipo exato, especialmente quando ele é complexo ou privado, e para permitir flexibilidade na implementação.
Por exemplo, uma função que retorna um iterador pode usar impl Iterator:
fn numeros_pares(limite: u32) -> impl Iterator<Item = u32> {
(0..limite).filter(|x| x % 2 == 0)
}
Nesse caso, o tipo exato do iterador (um Filter) é ocultado, mas o chamador pode usar qualquer método do trait Iterator. Isso simplifica a assinatura e permite mudar a implementação interna sem quebrar o código do chamador.
Uma limitação importante é que, com impl Trait, você só pode retornar um único tipo concreto. Se você tentar retornar tipos diferentes em branches diferentes, o compilador rejeitará. Para múltiplos tipos, você precisaria usar Box<dyn Trait>, que introduz overhead de alocação dinâmica.
// Correto: apenas um tipo concreto
fn retorna_i32() -> impl std::fmt::Display {
42
}
// Incorreto: dois tipos diferentes
fn retorna_condicional(flag: bool) -> impl std::fmt::Display {
if flag {
"string"
} else {
42 // erro: tipos diferentes
}
}
Outra característica é que impl Trait em retornos pode ser usado com traits que têm métodos associados, mas não com traits que exigem tipos associados (como Iterator com Item). Para esses casos, é necessário usar a sintaxe completa com impl Iterator<Item = u32>, como no exemplo acima.
Em resumo, impl Trait em retornos é uma ferramenta poderosa para abstração e encapsulamento, mas deve ser usada com cuidado para não limitar a flexibilidade.
Boas práticas
Ao trabalhar com esses tipos avançados, considere as seguintes boas práticas:
- Use o never type (
!) apenas em funções que realmente nunca retornam, comopanic!ou loops infinitos. Evite usá-lo para simular exceções, pois Rust não tem exceções. - Prefira usar referências e ponteiros inteligentes para DSTs, em vez de tentar criar DSTs próprios, a menos que seja absolutamente necessário e você entenda os detalhes de unsafe.
- Utilize aliases de tipo para melhorar a legibilidade, mas não crie aliases para tudo. Se um alias não adiciona clareza, é melhor usar o tipo original.
- Use
impl Traitem retornos para ocultar tipos complexos, mas lembre-se de que ele não suporta múltiplos tipos. Se precisar de polimorfismo dinâmico, considereBox<dyn Trait>. - Documente suas funções que usam
impl Trait, pois o tipo exato não é visível na assinatura.
Referências
- Never type - The Rust Reference
- The Rust Programming Language - Advanced Types
- Dynamically Sized Types - The Rust Reference
- The Rust Programming Language - Dynamically Sized Types
- Type Aliases - The Rust Reference
- The Rust Programming Language - Returning Types that Implement Traits
- Rust by Example - impl Trait
Exercícios
- Escreva uma função chamada
loop_infinitoque nunca retorna, usando o tipo!. (Dica: useloop {}).✓ Resposta:fn loop_infinito() -> ! { loop { // faz algo para sempre } } - Crie um alias de tipo para uma tupla de três elementos que represente coordenadas 3D (x, y, z) como
f64. Use esse alias em uma função que calcule a distância da origem.✓ Resposta:type Coordenada3D = (f64, f64, f64); fn distancia_da_origem(p: Coordenada3D) -> f64 { (p.0 * p.0 + p.1 * p.1 + p.2 * p.2).sqrt() } fn main() { let ponto = (1.0, 2.0, 2.0); println!("Distância: {}", distancia_da_origem(ponto)); } - Escreva uma função que retorne um iterador de números ímpares até um limite, usando
impl Iterator. Teste com um loop for.✓ Resposta:fn numeros_impares(limite: u32) -> impl Iterator<Item = u32> { (0..limite).filter(|x| x % 2 != 0) } fn main() { for num in numeros_impares(10) { println!("{}", num); } } - Explique por que
strnão pode ser usado como uma variável local sem referência ou Box. Dê um exemplo de uso correto.✓ Resposta:stré um DST, seu tamanho é desconhecido em tempo de compilação. Por isso, não pode ser usado como valor direto; precisa estar atrás de um ponteiro como&strouBox<str>. Exemplo:fn main() { let s: &str = "Olá"; // correto // let s: str = "Olá"; // erro: tamanho desconhecido } - Escreva uma função que use
impl Traitem retorno para retornar um tipo que implementastd::fmt::Display. Use-a com diferentes tipos e observe que o compilador só permite um tipo por função.✓ Resposta:
Se tentar retornarfn retorna_display() -> impl std::fmt::Display { 42 } fn main() { println!("{}", retorna_display()); }"texto"em um branch e42em outro, o compilador rejeitará, pois os tipos são diferentes.