Box<T> é um smart pointer que aloca um valor do tipo T na heap e armazena um ponteiro para ele na stack. Ele é um dos tipos fundamentais para gerenciamento de memória em Rust, permitindo que você coloque dados na heap de forma explícita e segura. Nesta aula, vamos entender como Box funciona, quando usá-lo e como ele interage com o sistema de ownership e borrowing.

Box é útil em várias situações: quando você precisa de um tipo de tamanho indeterminado em tempo de compilação, como em tipos recursivos; quando deseja transferir ownership de um grande objeto sem copiá-lo; ou quando precisa de um ponteiro para um trait object. Além disso, Box implementa os traits Deref e Drop, o que significa que você pode usá-lo como uma referência normal e a memória é liberada automaticamente quando sai de escopo.

Alocação na heap

Em Rust, a alocação na heap é feita através de tipos como Box, Vec, String, entre outros. Box<T> é a forma mais básica de alocar um valor na heap. Quando você cria um Box, o valor é movido para a heap e o Box armazena um ponteiro para ele. A sintaxe é simples: let b = Box::new(valor);. O valor original é consumido (movido) e você passa a usar o Box para acessá-lo.

A alocação na heap é útil quando o tamanho do dado não é conhecido em tempo de compilação, ou quando você precisa que o dado sobreviva ao escopo atual sem ser copiado. Por exemplo, ao retornar um grande struct de uma função, você pode usar Box para evitar uma cópia cara. Além disso, a heap permite alocar dados de tamanho dinâmico, como listas encadeadas ou árvores.

fn main() {
    // Aloca um inteiro na heap
    let b = Box::new(5);
    println!("b = {}", b); // acessa o valor através do Box
    // b é dropado automaticamente ao sair do escopo
}

O exemplo acima mostra a criação de um Box contendo o valor 5. O acesso ao valor é feito de forma transparente, como se fosse uma variável normal. Isso é possível graças à implementação do trait Deref, que veremos adiante.

Tipos recursivos

Tipos recursivos são tipos que contêm a si mesmos, direta ou indiretamente. Um exemplo clássico é uma lista encadeada: cada nó contém um valor e uma referência ao próximo nó. Em Rust, tipos recursivos não podem ser armazenados diretamente na stack, pois o compilador precisa saber o tamanho do tipo em tempo de compilação. Como um tipo recursivo pode ter tamanho infinito (teoricamente), Rust exige que você use um ponteiro (como Box) para quebrar a recursão.

Considere uma lista encadeada simples: List<T> pode ser vazia (Nil) ou um nó (Cons) que contém um valor e uma caixa para o resto da lista. Sem Box, o compilador não consegue determinar o tamanho de List, pois Cons conteria outro Cons, e assim por diante. Usando Box, o tamanho de Cons é fixo: ele contém um T e um ponteiro (Box) para o próximo nó, que é um tipo de tamanho conhecido (o ponteiro).

enum List {
    Nil,
    Cons(i32, Box<List>),
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    // processa a lista...
}

Neste exemplo, a enum List é recursiva: o variante Cons contém um Box<List>, que é um ponteiro para um nó na heap. Isso torna o tamanho de List finito e conhecido em tempo de compilação. Sem Box, o compilador rejeitaria o código com um erro de tamanho infinito.

Quando usar

Box é útil em várias situações, mas não é a única opção para alocação na heap. Aqui estão os cenários mais comuns:

  • Tipos recursivos: Como vimos, para quebrar a recursão em tipos como listas ou árvores.
  • Dados de tamanho dinâmico: Quando você não sabe o tamanho em tempo de compilação, como trait objects (ex: Box<dyn Trait>).
  • Evitar cópias grandes: Ao transferir ownership de um objeto grande, Box evita copiar o conteúdo na stack.
  • Ownership explícito da heap: Quando você precisa garantir que um valor seja alocado na heap e tenha um dono claro.
  • Interface com C: Às vezes, para interoperabilidade com código C, você precisa de ponteiros para a heap.

No entanto, Box não é adequado para todos os casos. Se você precisa de múltiplos donos, use Rc ou Arc. Se precisa de mutabilidade interior, use RefCell ou Mutex. Box é um smart pointer simples e eficiente, mas com funcionalidade limitada.

Deref

O trait Deref permite que um tipo seja tratado como uma referência ao seu conteúdo interno. Box<T> implementa Deref, o que significa que você pode usar o operador * para desreferenciar um Box e acessar o valor T diretamente. Além disso, Rust realiza a coerção de Deref automaticamente, permitindo que você chame métodos de T diretamente em um Box<T>.

A implementação de Deref para Box é simples: fn deref(&self) -> &T. Isso faz com que &Box<T> se comporte como &T. Por exemplo, você pode passar um &Box<T> para uma função que espera &T, graças à coerção de Deref.

fn main() {
    let b = Box::new(String::from("hello"));
    // deref implícito: podemos chamar métodos de String diretamente
    println!("Length: {}", b.len()); // b é &Box<String>, mas len() é de String
    // desreferencia explícita
    let s: &String = &*b; // *b devolve String, &*b devolve &String
    println!("{}", s);
}

Neste exemplo, b.len() funciona porque Rust aplica deref coercion: &Box<String> é convertido para &String, e então chama len(). Isso torna o código mais limpo e intuitivo.

Referências

Exercícios

  1. Crie um Box que armazena um inteiro 42 e imprima seu valor usando desreferência explícita.

    ✓ Resposta:
    fn main() {
        let b = Box::new(42);
        println!("{}", *b);
    }
    
  2. Defina uma enum recursiva para uma árvore binária (BinaryTree) onde cada nó contém um valor i32 e dois filhos (esquerdo e direito) que podem ser vazios. Use Box para os filhos.

    ✓ Resposta:
    enum BinaryTree {
        Empty,
        Node(i32, Box<BinaryTree>, Box<BinaryTree>),
    }
    
    fn main() {
        let tree = BinaryTree::Node(
            1,
            Box::new(BinaryTree::Node(2, Box::new(BinaryTree::Empty), Box::new(BinaryTree::Empty))),
            Box::new(BinaryTree::Empty),
        );
    }
    
  3. Escreva uma função que recebe um Box<i32> e retorna o valor dentro dele multiplicado por 2.

    ✓ Resposta:
    fn double(b: Box<i32>) -> i32 {
        *b * 2
    }
    
    fn main() {
        let b = Box::new(10);
        println!("{}", double(b)); // 20
    }
    
  4. Crie um vetor de Box<dyn Fn(i32) -> i32> contendo duas funções: uma que dobra o valor e outra que eleva ao quadrado. Itere sobre o vetor e aplique cada função ao valor 5, imprimindo os resultados.

    ✓ Resposta:
    fn main() {
        let funcs: Vec<Box<dyn Fn(i32) -> i32>> = vec![
            Box::new(|x| x * 2),
            Box::new(|x| x * x),
        ];
        for f in &funcs {
            println!("{}", f(5));
        }
    }
    
  5. Explique por que o código abaixo não compila e como corrigi-lo usando Box:

    enum List {
        Nil,
        Cons(i32, List),
    }
    

    ✓ Resposta:O código não compila porque List é um tipo recursivo sem indireção. O compilador não consegue determinar o tamanho de List, pois Cons contém um List que pode conter outro Cons, etc. Para corrigir, use Box<List> no lugar de List dentro de Cons: Cons(i32, Box<List>). Isso torna o tamanho finito, pois Box é um ponteiro de tamanho fixo.

Boas práticas e observações finais

Ao usar Box, lembre-se de que ele é um smart pointer com ownership único. Não há contagem de referências, então cada Box tem um único dono. Se precisar de múltiplos donos, considere Rc ou Arc. Além disso, Box é útil para trait objects, mas tenha em mente que isso envolve dispatch dinâmico, o que pode ter um pequeno custo de performance. Prefira genéricos quando possível. Por fim, sempre que você usar Box, certifique-se de que não há necessidade de um tipo mais complexo como Rc ou RefCell.