Smart Pointers Rust: Box, Rc, Arc, RefCell | Rust Brasil

Guia completo de smart pointers em Rust: Box, Rc, Arc e RefCell. Quando usar, Send/Sync e segurança entre threads.

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