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, como panic! 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 Trait em retornos para ocultar tipos complexos, mas lembre-se de que ele não suporta múltiplos tipos. Se precisar de polimorfismo dinâmico, considere Box<dyn Trait>.
  • Documente suas funções que usam impl Trait, pois o tipo exato não é visível na assinatura.

Referências

Exercícios

  1. Escreva uma função chamada loop_infinito que nunca retorna, usando o tipo !. (Dica: use loop {}).

    ✓ Resposta:
    fn loop_infinito() -> ! {
        loop {
            // faz algo para sempre
        }
    }
    
  2. 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));
    }
    
  3. 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);
        }
    }
    
  4. Explique por que str nã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 &str ou Box<str>. Exemplo:
    fn main() {
        let s: &str = "Olá"; // correto
        // let s: str = "Olá"; // erro: tamanho desconhecido
    }
    
  5. Escreva uma função que use impl Trait em retorno para retornar um tipo que implementa std::fmt::Display. Use-a com diferentes tipos e observe que o compilador só permite um tipo por função.

    ✓ Resposta:
    fn retorna_display() -> impl std::fmt::Display {
        42
    }
    
    fn main() {
        println!("{}", retorna_display());
    }
    
    Se tentar retornar "texto" em um branch e 42 em outro, o compilador rejeitará, pois os tipos são diferentes.