Smart pointers (ponteiros inteligentes) são estruturas de dados que se comportam como ponteiros, mas possuem metadados e capacidades adicionais. Em Rust, eles são fundamentais para cenários onde o sistema de ownership padrão não é suficiente — como estruturas de dados recursivas, contagem de referência ou mutabilidade interior. Neste artigo, vamos explorar cada smart pointer em detalhes, entender como funcionam por dentro e quando usar cada um.
Por Que Precisamos de Smart Pointers?
O sistema de ownership do Rust é poderoso, mas tem limitações práticas:
- Tamanho desconhecido em tempo de compilação: tipos recursivos não têm tamanho fixo
- Múltiplos donos: às vezes, vários trechos de código precisam acessar o mesmo dado
- Mutabilidade controlada: você pode precisar mutar dados mesmo tendo apenas uma referência imutável
Smart pointers resolvem cada um desses cenários com abstrações seguras e eficientes.
Box<T> — Alocação no Heap
Box<T> é o smart pointer mais simples: aloca dados no heap e mantém um ponteiro na stack.
Layout de Memória
Stack Heap
┌──────────┐ ┌──────────┐
│ Box<i32> │──────>│ 42 │
│ (ponteiro)│ │ (i32) │
│ 8 bytes │ │ 4 bytes │
└──────────┘ └──────────┘
Quando Usar Box
1. Tipos recursivos:
// NÃO COMPILA — tamanho infinito
// enum Lista {
// Cons(i32, Lista),
// Nil,
// }
// COMPILA — Box tem tamanho fixo (1 ponteiro)
#[derive(Debug)]
enum Lista {
Cons(i32, Box<Lista>),
Nil,
}
fn main() {
use Lista::{Cons, Nil};
let lista = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", lista);
// Cons(1, Cons(2, Cons(3, Nil)))
}
2. Trait objects com tamanho dinâmico:
trait Animal {
fn som(&self) -> &str;
fn nome(&self) -> &str;
}
struct Gato;
struct Cachorro;
impl Animal for Gato {
fn som(&self) -> &str { "Miau" }
fn nome(&self) -> &str { "Gato" }
}
impl Animal for Cachorro {
fn som(&self) -> &str { "Au au" }
fn nome(&self) -> &str { "Cachorro" }
}
fn main() {
let animais: Vec<Box<dyn Animal>> = vec![
Box::new(Gato),
Box::new(Cachorro),
];
for animal in &animais {
println!("{} faz {}", animal.nome(), animal.som());
}
}
3. Transferir ownership de dados grandes sem copiar:
fn processar(dados: Box<[u8; 1_000_000]>) {
println!("Processando {} bytes", dados.len());
}
fn main() {
let dados = Box::new([0u8; 1_000_000]); // 1MB no heap, não na stack
processar(dados); // move o ponteiro, não os dados
}
Rc<T> — Contagem de Referência (Single-Thread)
Rc<T> (Reference Counting) permite que múltiplos donos compartilhem ownership de um dado. Um contador interno rastreia quantas referências existem, e o dado só é destruído quando o último Rc é descartado.
Layout de Memória
Stack Heap
┌──────────┐ ┌──────────────┐
│ Rc (a) │──┐ │ strong: 3 │
└──────────┘ │ │ weak: 0 │
├───────>│──────────────│
┌──────────┐ │ │ dados: T │
│ Rc (b) │──┤ │ │
└──────────┘ │ └──────────────┘
│
┌──────────┐ │
│ Rc (c) │──┘
└──────────┘
Exemplo Prático: Grafo Simples
use std::rc::Rc;
#[derive(Debug)]
struct No {
valor: i32,
vizinhos: Vec<Rc<No>>,
}
fn main() {
let compartilhado = Rc::new(No {
valor: 1,
vizinhos: vec![],
});
println!("Contagem após criação: {}", Rc::strong_count(&compartilhado));
let no_a = Rc::new(No {
valor: 2,
vizinhos: vec![Rc::clone(&compartilhado)],
});
let no_b = Rc::new(No {
valor: 3,
vizinhos: vec![Rc::clone(&compartilhado)],
});
println!("Contagem após clones: {}", Rc::strong_count(&compartilhado));
// Contagem após clones: 3
drop(no_a);
println!("Contagem após drop de no_a: {}", Rc::strong_count(&compartilhado));
// Contagem após drop de no_a: 2
drop(no_b);
println!("Contagem após drop de no_b: {}", Rc::strong_count(&compartilhado));
// Contagem após drop de no_b: 1
}
Rc::clone vs .clone()
Rc::clone(&rc) não clona os dados — apenas incrementa o contador. É uma operação O(1). Use Rc::clone() em vez de rc.clone() para deixar clara a intenção.
Weak<T> — Evitando Ciclos
Rc pode criar ciclos de referência que causam memory leaks. Use Weak para referências que não mantêm o dado vivo:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Pessoa {
nome: String,
melhor_amigo: RefCell<Option<Weak<Pessoa>>>,
}
impl Drop for Pessoa {
fn drop(&mut self) {
println!("{} foi desalocado", self.nome);
}
}
fn main() {
let ana = Rc::new(Pessoa {
nome: "Ana".into(),
melhor_amigo: RefCell::new(None),
});
let carlos = Rc::new(Pessoa {
nome: "Carlos".into(),
melhor_amigo: RefCell::new(None),
});
// Referências fracas — não criam ciclo
*ana.melhor_amigo.borrow_mut() = Some(Rc::downgrade(&carlos));
*carlos.melhor_amigo.borrow_mut() = Some(Rc::downgrade(&ana));
// Acessando o Weak
if let Some(amigo) = ana.melhor_amigo.borrow().as_ref() {
if let Some(amigo_rc) = amigo.upgrade() {
println!("Melhor amigo de Ana: {}", amigo_rc.nome);
}
}
} // Ana e Carlos são desalocados corretamente
Arc<T> — Contagem de Referência Thread-Safe
Arc<T> (Atomic Reference Counting) funciona como Rc<T>, mas usa operações atômicas para ser seguro entre threads. O custo é pequeno, mas mensurável.
use std::sync::Arc;
use std::thread;
fn main() {
let dados = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let dados_clone = Arc::clone(&dados);
let handle = thread::spawn(move || {
let soma: i32 = dados_clone.iter().sum();
println!("Thread {}: soma = {}", i, soma);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
Arc + Mutex — Mutabilidade Compartilhada entre Threads
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contador_clone = Arc::clone(&contador);
let handle = thread::spawn(move || {
let mut num = contador_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap());
// Resultado: 10
}
RefCell<T> — Mutabilidade Interior
RefCell<T> permite mutar dados mesmo quando só temos uma referência imutável. As regras de borrowing são verificadas em tempo de execução em vez de compilação.
Regras de Borrowing em Runtime
RefCell — verificação em runtime:
┌─────────────────────────────────────────────┐
│ .borrow() → Ref<T> (imutável) │
│ .borrow_mut() → RefMut<T> (mutável) │
│ │
│ Múltiplos .borrow() → OK │
│ Um .borrow_mut() → OK │
│ .borrow() + .borrow_mut() → PANIC! │
│ Múltiplos .borrow_mut() → PANIC! │
└─────────────────────────────────────────────┘
Exemplo: Cache com Mutabilidade Interior
use std::cell::RefCell;
use std::collections::HashMap;
struct Cache {
dados: RefCell<HashMap<String, String>>,
}
impl Cache {
fn novo() -> Self {
Cache {
dados: RefCell::new(HashMap::new()),
}
}
// Note: &self (imutável!), mas consegue mutar o HashMap interno
fn obter_ou_calcular(&self, chave: &str) -> String {
// Verifica se já existe
if let Some(valor) = self.dados.borrow().get(chave) {
return valor.clone();
}
// Calcula e insere
let valor = format!("calculado_{}", chave);
self.dados.borrow_mut().insert(chave.to_string(), valor.clone());
valor
}
}
fn main() {
let cache = Cache::novo();
println!("{}", cache.obter_ou_calcular("x")); // calculado_x
println!("{}", cache.obter_ou_calcular("y")); // calculado_y
println!("{}", cache.obter_ou_calcular("x")); // calculado_x (do cache)
println!("Cache: {:?}", cache.dados.borrow());
}
Combinando Smart Pointers: Rc<RefCell<T>>
A combinação Rc<RefCell<T>> é extremamente comum — permite múltiplos donos com mutabilidade:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Conta {
titular: String,
saldo: f64,
}
type ContaCompartilhada = Rc<RefCell<Conta>>;
fn depositar(conta: &ContaCompartilhada, valor: f64) {
conta.borrow_mut().saldo += valor;
}
fn sacar(conta: &ContaCompartilhada, valor: f64) -> Result<(), String> {
let mut c = conta.borrow_mut();
if c.saldo >= valor {
c.saldo -= valor;
Ok(())
} else {
Err(format!("Saldo insuficiente: {:.2}", c.saldo))
}
}
fn main() {
let conta = Rc::new(RefCell::new(Conta {
titular: "Maria".into(),
saldo: 1000.0,
}));
// Simulando múltiplos "serviços" acessando a mesma conta
let servico_deposito = Rc::clone(&conta);
let servico_saque = Rc::clone(&conta);
depositar(&servico_deposito, 500.0);
println!("Após depósito: {:.2}", conta.borrow().saldo); // 1500.00
sacar(&servico_saque, 200.0).unwrap();
println!("Após saque: {:.2}", conta.borrow().saldo); // 1300.00
}
Tabela Comparativa
┌──────────────┬──────────┬──────────────┬──────────────┬────────────┐
│ Tipo │ Heap? │ Múltiplos │ Mutabilidade │ Thread- │
│ │ │ Donos? │ Interior? │ Safe? │
├──────────────┼──────────┼──────────────┼──────────────┼────────────┤
│ Box<T> │ Sim │ Não │ Não │ Sim* │
│ Rc<T> │ Sim │ Sim │ Não │ Não │
│ Arc<T> │ Sim │ Sim │ Não │ Sim │
│ RefCell<T> │ Não** │ Não │ Sim │ Não │
│ Mutex<T> │ Não** │ Não │ Sim │ Sim │
│ Rc<RefCell> │ Sim │ Sim │ Sim │ Não │
│ Arc<Mutex> │ Sim │ Sim │ Sim │ Sim │
└──────────────┴──────────┴──────────────┴──────────────┴────────────┘
* Box<T> é Send/Sync se T for Send/Sync
** RefCell/Mutex não alocam no heap por si — dependem de onde são criados
Erros Comuns
1. Usar Rc entre threads
use std::rc::Rc;
// use std::thread;
// NÃO COMPILA: Rc não é Send
// let dados = Rc::new(42);
// thread::spawn(move || println!("{}", dados));
// SOLUÇÃO: Use Arc
use std::sync::Arc;
use std::thread;
fn main() {
let dados = Arc::new(42);
let dados_clone = Arc::clone(&dados);
thread::spawn(move || println!("{}", dados_clone)).join().unwrap();
}
2. Panic por borrow duplo em RefCell
use std::cell::RefCell;
fn main() {
let dados = RefCell::new(vec![1, 2, 3]);
let r1 = dados.borrow();
// let r2 = dados.borrow_mut(); // PANIC em runtime!
// SOLUÇÃO: Garanta que o borrow anterior termine antes
drop(r1);
let r2 = dados.borrow_mut();
println!("{:?}", r2);
// Ou use try_borrow_mut para evitar panic
drop(r2);
let _r3 = dados.borrow();
match dados.try_borrow_mut() {
Ok(mut r) => r.push(4),
Err(e) => println!("Não foi possível emprestar: {}", e),
}
}
3. Memory leak com ciclos em Rc
Se dois Rc apontam um para o outro, nenhum deles será desalocado. Use Weak para quebrar ciclos (como mostrado na seção sobre Weak<T>).
Aplicações no Mundo Real
Árvore com nós compartilhados
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct TreeNode {
valor: i32,
pai: RefCell<Weak<TreeNode>>,
filhos: RefCell<Vec<Rc<TreeNode>>>,
}
fn main() {
let raiz = Rc::new(TreeNode {
valor: 1,
pai: RefCell::new(Weak::new()),
filhos: RefCell::new(vec![]),
});
let filho = Rc::new(TreeNode {
valor: 2,
pai: RefCell::new(Rc::downgrade(&raiz)),
filhos: RefCell::new(vec![]),
});
raiz.filhos.borrow_mut().push(Rc::clone(&filho));
println!("Raiz: {}", raiz.valor);
println!("Filho: {}", filho.valor);
if let Some(pai) = filho.pai.borrow().upgrade() {
println!("Pai do filho: {}", pai.valor);
}
}
Veja Também
- Ownership e Borrowing: O Coração do Rust — base para entender por que smart pointers existem
- Entendendo Lifetimes em Rust — alternativas com referências e lifetimes
- Trait Objects vs Generics em Rust —
Box<dyn Trait>em detalhe - Async/Await em Rust —
Arc<Mutex<T>>em contextos assíncronos - Glossário Rust em Português — definições de termos técnicos