Smart pointers do Rust explicados: Box, Rc, Arc e RefCell em 2026
Os smart pointers do Rust Box, Rc, Arc e RefCell explicados com exemplos compiláveis de 2026, uma tabela de decisão e as perguntas de entrevista mais comuns.

Os smart pointers do Rust são as ferramentas que liberam padrões de posse que o borrow checker sozinho não consegue expressar: alocação no heap, posse compartilhada e mutação por trás de uma referência compartilhada. Onde uma referência simples (&T) apenas toma emprestado um valor, um smart pointer possui seus dados e adiciona comportamento extra por cima. Este guia detalha os quatro tipos que todo desenvolvedor Rust encontra em produção e em entrevistas: Box, Rc, Arc e RefCell, com exemplos compiláveis voltados para a edição Rust 2024.
Usar Box<T> para alocação no heap com um único dono, Rc<T> para posse compartilhada em uma única thread, Arc<T> para posse compartilhada entre threads, e RefCell<T> para mutar um valor através de uma referência compartilhada. Os pares Rc<RefCell<T>> e Arc<Mutex<T>> cobrem o estado mutável compartilhado.
O que são smart pointers no Rust
Um smart pointer é uma struct que se comporta como um ponteiro, mas carrega metadados ou capacidades extras. A maioria implementa a trait Deref, de modo que *pointer e as chamadas de método funcionam como se o ponteiro fosse uma referência simples, e a trait Drop, de modo que a limpeza roda automaticamente quando o valor sai do escopo. A biblioteca padrão fornece os quatro tipos cobertos aqui, e compreendê-los depende de um domínio sólido de posse e empréstimo, que decidem quem libera cada alocação e quando.
A distinção principal em relação a uma referência é a posse. &T nunca possui os dados para os quais aponta, então não pode viver mais que o valor. Um smart pointer possui os dados, controla o tempo de vida deles e os libera de forma determinística. O capítulo do Rust Book sobre smart pointers os trata como uma categoria justamente porque compartilham esse formato de posse mais comportamento.
Box<T>: alocação no heap para tipos recursivos e com tamanho
Box<T> é o smart pointer mais simples. Ele guarda um valor no heap e mantém um ponteiro para ele na pilha, com um único dono e sem sobrecarga em tempo de execução além da própria alocação. Sua tarefa mais comum é dar um tamanho conhecido a tipos recursivos: um tipo que contém a si mesmo diretamente seria infinitamente grande, mas um Box é apenas um ponteiro, então seu tamanho é fixo independentemente do que ele aponta.
O exemplo abaixo define uma árvore binária. Cada Node contém dois filhos, e sem indireção o compilador não consegue calcular o tamanho de Tree. Envolver cada filho em um Box quebra a recursão no nível do tipo.
#[derive(Debug)]
enum Tree {
Leaf(i32),
// Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
Node(Box<Tree>, Box<Tree>),
}
fn sum(tree: &Tree) -> i32 {
match tree {
Tree::Leaf(value) => *value, // base case: return the leaf
Tree::Node(left, right) => sum(left) + sum(right), // recurse into both children
}
}
fn main() {
// Build (1) + ((2) + (3)) = 6
let tree = Tree::Node(
Box::new(Tree::Leaf(1)),
Box::new(Tree::Node(
Box::new(Tree::Leaf(2)),
Box::new(Tree::Leaf(3)),
)),
);
println!("sum = {}", sum(&tree)); // sum = 6
}Box também importa quando mover um valor grande seria custoso, ou quando uma função retorna um objeto de trait como Box<dyn Error>. Em todos os casos a regra é a mesma: um único dono, liberado automaticamente quando o Box é destruído.
Rc<T>: posse compartilhada em código de thread única
Às vezes um valor precisa de vários donos, e nenhum deles é claramente o último a usá-lo. Rc<T> (contado por referência) resolve isso mantendo uma contagem forte de quantos donos existem. Rc::clone incrementa essa contagem sem copiar os dados subjacentes, e cada destruição a decrementa. Quando a contagem chega a zero, o valor é liberado.
Considere um objeto de configuração que vários workers leem. Cada worker deve manter a configuração pelo tempo que precisar, e a alocação só deve desaparecer quando o último worker se for.
use std::rc::Rc;
#[derive(Debug)]
struct Config {
endpoint: String,
timeout_ms: u32,
}
fn main() {
// Rc::new moves Config onto the heap with a strong count of 1.
let config = Rc::new(Config {
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
});
// Rc::clone only bumps the reference count; it does not deep-copy Config.
let worker_a = Rc::clone(&config);
let worker_b = Rc::clone(&config);
// All three handles point to the same allocation.
println!("endpoint: {}", worker_a.endpoint);
println!("timeout: {}", worker_b.timeout_ms);
// strong_count reports how many owners currently hold the value.
println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}Chamar Rc::clone explicitamente (em vez de config.clone()) é idiomático: sinaliza ao leitor que a operação é barata e só mexe em um contador. A pegadinha é que Rc<T> só distribui referências compartilhadas e imutáveis. Ele não pode mutar o valor que compartilha, e não é seguro enviá-lo entre threads. A documentação de Rc detalha as duas restrições.
RefCell<T> e a mutabilidade interior no Rust
O Rust normalmente impõe que um valor seja compartilhado por muitas referências imutáveis ou mutado por exatamente uma referência mutável, e verifica isso em tempo de compilação. RefCell<T> fornece mutabilidade interior: permite que o código mute um valor através de uma referência compartilhada movendo essa mesma verificação para o tempo de execução. borrow() retorna um guard de leitura compartilhado; borrow_mut() retorna um guard de escrita exclusivo. As regras são idênticas, mas as violações causam um pânico em vez de falhar na compilação.
Combinado com Rc, isso produz o padrão canônico de mutabilidade compartilhada em thread única, Rc<RefCell<T>>: vários donos que podem todos atualizar o mesmo valor.
use std::rc::Rc;
use std::cell::RefCell;
// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;
fn increment(counter: &SharedCounter) {
// borrow_mut() hands out an exclusive reference, checked at runtime.
*counter.borrow_mut() += 1;
}
fn main() {
let counter: SharedCounter = Rc::new(RefCell::new(0));
let handle_a = Rc::clone(&counter);
let handle_b = Rc::clone(&counter);
increment(&handle_a);
increment(&handle_b);
increment(&counter);
// borrow() gives a shared read guard; the value is now 3.
println!("count = {}", counter.borrow()); // count = 3
}Chamar borrow_mut() enquanto outro guard borrow() ou borrow_mut() ainda está vivo causa um pânico com already borrowed: BorrowMutError. As garantias de segurança se mantêm, mas um erro de lógica que o compilador teria detectado vira uma falha em tempo de execução. Convém manter os guards de vida curta e evitar reter um empréstimo através de uma chamada de função que possa reentrar no mesmo RefCell.
A mutabilidade interior é também onde os ciclos de referência se escondem. Dois valores Rc<RefCell<T>> que apontam um para o outro mantêm a contagem forte um do outro acima de zero para sempre, então nenhum é liberado. A solução é Weak<T>, um handle sem posse que não afeta a contagem forte; o passo a passo clássico está em Learn Rust With Entirely Too Many Linked Lists.
Arc<T>: contagem de referências thread-safe para concorrência
Arc<T> (contado por referência de forma atômica) é o irmão multithread de Rc<T>. A API pública é quase idêntica, mas o contador usa operações atômicas, de modo que clonar e destruir um Arc a partir de várias threads ao mesmo tempo continua correto. Essa atomicidade é o motivo pelo qual Arc é escolhido apenas quando o compartilhamento cruza uma fronteira de thread: o incremento atômico é sensivelmente mais lento que o simples que Rc usa.
Como Arc<T> ainda distribui referências imutáveis, mutar entre threads exige uma primitiva de sincronização. Mutex<T> é o parceiro habitual, dando o padrão Arc<Mutex<T>> que espelha Rc<RefCell<T>> para o código concorrente. O exemplo dispara quatro threads que incrementam cada uma um total compartilhado mil vezes.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc is safe to share across threads; Mutex serializes access to the inner value.
let total = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..4 {
// Each thread gets its own Arc handle (an atomic count bump).
let total = Arc::clone(&total);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() blocks until the mutex is free, then returns a guard.
let mut value = total.lock().unwrap();
*value += 1;
}
}));
}
for handle in handles {
handle.join().unwrap(); // wait for every thread before reading
}
println!("total = {}", *total.lock().unwrap()); // total = 4000
}O compilador impõe a fronteira por você: Rc<T> não é Send, então tentar mover um para dentro de thread::spawn não compila, empurrando na direção de Arc. Recorrer a Arc dentro de uma única thread apenas paga por atômicos que nunca são usados. A documentação de Arc cobre em detalhe as garantias de ordenação de memória, e Arc<Mutex<T>> sustenta boa parte do estado compartilhado no Rust assíncrono com Tokio.
Pronto para mandar bem nas entrevistas de Rust?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Box vs Rc vs Arc vs RefCell: quando usar cada um
Os quatro tipos se combinam ao longo de dois eixos: quantos donos um valor tem, e se ele pode ser mutado através de um handle compartilhado. A tabela resume os compromissos.
| Tipo | Posse | Mutação via handle compartilhado | Thread-safe | Sobrecarga |
|------|-----------|----------------------------|-------------|----------|
| Box<T> | Único | Não | Sim, se T: Send | Apenas alocação no heap |
| Rc<T> | Compartilhada | Não | Não | Contagem não atômica |
| Arc<T> | Compartilhada | Não | Sim | Contagem atômica |
| RefCell<T> | Único | Sim (verificada em execução) | Não | Flag de empréstimo em execução |
A árvore de decisão prática é curta. Precisa de um único dono no heap: Box. Precisa de vários donos em uma única thread: Rc. Precisa de vários donos entre threads: Arc. Precisa mutar um valor compartilhado: envolver o tipo interno em RefCell (thread única) ou Mutex (multithread). A orientação do Effective Rust sobre os tipos de referência e ponteiro chega à mesma estratificação. Convém começar pela opção menos poderosa e adicionar capacidade só quando o compilador obrigar.
Perguntas de entrevista sobre smart pointers do Rust
Smart pointers são um tema de entrevista confiável porque testam se um candidato entende posse e não apenas sintaxe. As perguntas abaixo aparecem com frequência, e há mais reunidas no módulo de entrevista sobre smart pointers do Rust.
Qual é a diferença entre Rc e Arc? Ambos fornecem posse compartilhada por contagem de referências. Rc usa um contador não atômico e fica confinado a uma única thread; Arc usa operações atômicas e pode ser compartilhado entre threads a um pequeno custo em execução. O compilador impõe a divisão: Rc não é nem Send nem Sync, então não pode cruzar uma fronteira de thread.
Por que combinar Rc com RefCell? Rc concede posse compartilhada, mas apenas acesso imutável. RefCell adiciona mutabilidade interior, permitindo que os donos mutem o valor através de uma referência compartilhada com verificações de empréstimo em execução. Juntos, Rc<RefCell<T>> é o bloco idiomático de mutabilidade compartilhada em thread única.
Como a contagem de referências pode vazar memória no Rust? Dois valores Rc (ou Arc) que se referenciam mutuamente formam um ciclo cujas contagens fortes nunca chegam a zero, então a alocação nunca é liberada. Quebrar o ciclo com Weak<T>, que mantém uma referência sem posse, restaura a limpeza correta.
Quando Box é preferível a Rc? Sempre que um único dono for suficiente. Box não tem sobrecarga de contagem de referências, então é a escolha padrão para alocação no heap, tipos recursivos e objetos de trait. Só convém recorrer a Rc quando posse compartilhada real for necessária.
Conclusão
Os smart pointers do Rust transformam a posse, de restrição em um conjunto de blocos combináveis. A escolha entre eles decorre diretamente do formato do problema:
- Recorrer a
Box<T>quando um valor precisa de um único dono no heap, quando um tipo recursivo precisa de tamanho fixo, ou quando uma função retorna um objeto de trait. - Usar
Rc<T>para vários donos em uma única thread, e mudar paraArc<T>no momento em que a posse cruza uma fronteira de thread. - Adicionar
RefCell<T>para mutabilidade interior em código de thread única, eMutex<T>para o equivalente concorrente, mantendo curtos os guards de empréstimo e de bloqueio. - Combiná-los deliberadamente:
Rc<RefCell<T>>para estado mutável compartilhado em uma única thread,Arc<Mutex<T>>entre threads. - Ficar atento a ciclos de referência entre ponteiros contados e quebrá-los com
Weak<T>para evitar vazamentos. - Optar por padrão pelo tipo menos poderoso e deixar o compilador indicar quando mais capacidade é necessária.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Ownership e Borrowing em Rust: Guia Completo para Entrevistas Tecnicas
Domine os conceitos de ownership, borrowing e lifetimes em Rust. Guia pratico com exemplos de codigo para se preparar para entrevistas tecnicas de programacao de sistemas.

Perguntas de Entrevista sobre Rust: Guia Completo 2026
As 25 perguntas mais comuns em entrevistas sobre Rust. Ownership, borrowing, lifetimes, traits, async e concorrencia com respostas detalhadas e exemplos de codigo.

Rust 2026 Edition: Traits, Generics e Perguntas Avancadas para Entrevistas Tecnicas
Domine traits, generics, dispatch estatico e dinamico, async closures e RPITIT no Rust 2026 Edition. Guia completo com exemplos praticos para entrevistas tecnicas avancadas.