Box<T>
Nesta aula, exploramos o smart pointer Box<T> em Rust, que permite alocar dados na heap. Aprendemos como utilizá-lo para tipos recursivos como listas encadeadas, quando empregá-lo e como a trait Deref possibilita a desreferenciação automática.
Box<T> é um dos smart pointers mais simples e fundamentais em Rust. Ele permite armazenar dados na heap em vez da stack, mantendo um ponteiro para esses dados. Isso é útil quando você precisa de alocação dinâmica, especialmente para tipos cujo tamanho não pode ser conhecido em tempo de compilação ou para transferir propriedade sem copiar grandes quantidades de dados.
Nesta aula, veremos como Box<T> funciona, seus usos comuns, e como a trait Deref permite que ele se comporte como uma referência normal.
Alocação na heap
Em Rust, a alocação padrão é na stack, que é rápida e gerenciada automaticamente. No entanto, algumas situações exigem alocação na heap, como quando o tamanho do dado é dinâmico ou quando você precisa que o dado viva além do escopo da função que o criou. Box<T> é a maneira mais direta de alocar na heap.
Para criar um Box, use Box::new(valor). O valor será movido para a heap e o Box retornado contém um ponteiro para ele. Quando o Box sai de escopo, o dado na heap é liberado automaticamente (drop).
fn main() {
let b = Box::new(5); // aloca um inteiro na heap
println!("b = {}", b); // imprime 5
} // b sai de escopo, memória liberada
O Box em si (o ponteiro) fica na stack, mas o dado (o inteiro) está na heap. Isso permite que você tenha um tamanho fixo para o Box (tamanho de um ponteiro), enquanto o dado pode ser de qualquer tamanho.
Tipos recursivos
Um tipo recursivo é aquele que contém a si mesmo como campo. Por exemplo, uma lista encadeada: cada nó contém um valor e uma referência ao próximo nó. Em Rust, o compilador precisa saber o tamanho de cada tipo em tempo de compilação. Para tipos recursivos, o tamanho seria infinito se não houvesse indireção. Box<T> resolve isso: ao invés de armazenar o próximo nó diretamente, armazenamos um Box<Node>, que tem tamanho fixo (ponteiro).
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Sem o Box, o compilador não saberia o tamanho de List, pois seria infinitamente recursivo. O Box quebra essa recursão, pois cada nó contém um ponteiro para o próximo, e não o próximo nó inteiro.
Quando usar
Box<T> é útil em várias situações:
- Tipos recursivos: como visto, para listas, árvores, etc.
- Dados grandes na heap: para evitar cópias custosas ao transferir propriedade. Por exemplo, mover um Box<T> é barato (apenas copia o ponteiro), enquanto mover o T diretamente pode ser caro se T for grande.
- Traits objects: quando você precisa de polimorfismo dinâmico, usando
Box<dyn Trait>. - Recursão com tipos Sized: quando você precisa de um tipo que implementa
Sizedmas o tamanho real é desconhecido em tempo de compilação.
No entanto, Box não é a única opção. Para alocação dinâmica, existem Rc, Arc, Cell, RefCell, etc. Box é mais adequado quando você precisa de propriedade única e alocação na heap.
Deref
Box<T> implementa a trait Deref, que permite que você use o operador * para acessar o valor interno. Além disso, Rust aplica coerção de desreferência (deref coercion), que converte automaticamente referências a Box em referências ao valor contido. Isso permite que você chame métodos do T diretamente no Box.
fn main() {
let x = Box::new(5);
// *x desreferencia para obter o valor
assert_eq!(*x, 5);
// coerção de desreferência: podemos chamar métodos de i32 diretamente
let y: &i32 = &x; // &Box<i32> é coercido para &i32
println!("y = {}", y);
}
A trait Deref também permite que Box seja usado em contextos onde uma referência ao T é esperada, como em parâmetros de função. Por exemplo, você pode passar um &Box<T> para uma função que espera &T.
Boas práticas
Use Box quando precisar de alocação na heap com propriedade única. Para dados compartilhados, considere Rc ou Arc. Evite usar Box desnecessariamente, pois alocação na heap tem custo. Prefira tipos na stack quando possível.
Referências
- Documentação oficial de Box
- The Rust Programming Language - Capítulo 15.1: Box<T>
- Rust by Example: Box
- The Rustonomicon: Box
- Trait Deref
Exercícios
Crie um Box que armazena uma String e imprima seu valor.
✓ Resposta:fn main() { let b = Box::new(String::from("Hello")); println!("{}", b); }Defina um enum recursivo para uma árvore binária usando Box.
✓ Resposta:enum BinaryTree { Empty, Node(i32, Box<BinaryTree>, Box<BinaryTree>), }Escreva uma função que aceita um Box<i32> e retorna o valor multiplicado por 2. Use coerção de desreferência para chamar a função com um &i32.
✓ Resposta:fn dobro(x: &i32) -> i32 { x * 2 } fn main() { let b = Box::new(5); println!("{}", dobro(&b)); // coerção automática }Crie uma lista encadeada simples usando Box e implemente uma função que imprime todos os elementos.
✓ Resposta:enum List { Cons(i32, Box<List>), Nil, } fn print_list(list: &List) { match list { Cons(val, next) => { println!("{}", val); print_list(next); } Nil => {} } } fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); print_list(&list); }Explique por que Box<T> é necessário para tipos recursivos. Dê um exemplo de código que não compila sem Box.
✓ Resposta:O compilador precisa saber o tamanho de cada tipo em tempo de compilação. Um tipo recursivo sem indireção teria tamanho infinito. Box fornece uma indireção (ponteiro) de tamanho fixo. Exemplo que não compila:enum List { Cons(i32, List), // erro: recursão sem indireção Nil, }