Box<T>
Nesta aula, exploramos o smart pointer Box<T> em Rust, que permite alocar dados na heap. Abordamos sua utilidade para tipos recursivos, o padrão de implementação Deref para acesso transparente ao valor interno e as situações ideais para seu uso.
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
- Documentação oficial de Box<T>
- Capítulo 15.1 do Livro de Rust: Box<T>
- Trait Deref na documentação
- Rust by Example: Box
- The Rustonomicon: Alocação na heap
- Referência de tipos ponteiro
Exercícios
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); }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), ); }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 }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)); } }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.