Rust

Smart Pointers — Box, Rc e RefCell Já leu

15 min de leitura

Smart Pointers — Box, Rc e RefCell
  Até agora trabalhamos com dados na stack e referências simples com &. Mas alguns problemas exigem mais flexibilidade: dados cujo t

 

Até agora trabalhamos com dados na stack e referências simples com &. Mas alguns problemas exigem mais flexibilidade: dados cujo tamanho só é conhecido em tempo de execução, valores que precisam ter múltiplos donos, ou mutabilidade em contextos onde o compilador não consegue verificar as regras de borrowing estaticamente.

Para esses casos, Rust oferece smart pointers — tipos que se comportam como ponteiros mas carregam metadados e garantias adicionais. Ao contrário de C++, os smart pointers de Rust são seguros por construção. Neste artigo vamos explorar os três mais fundamentais: Box, Rc e RefCell.


Box — alocação no heap com ownership único

Box é o smart pointer mais simples: aloca um valor no heap e mantém um ponteiro para ele na stack. Quando o Box sai de escopo, o valor no heap é destruído automaticamente.

fn main() {
    // Sem Box: i32 na stack
    let x = 5;

    // Com Box: i32 no heap, ponteiro na stack
    let y = Box::new(5);

    println!("x = {x}");
    println!("y = {y}"); // Box implementa Display — desreferencia automaticamente

    // Desreferenciação explícita
    println!("*y = {}", *y);

    // Comparação funciona diretamente
    assert_eq!(x, *y);
} // y sai de escopo — heap liberado automaticamente

Quando usar Box

Caso 1: tipos de tamanho desconhecido em tempo de compilação

O compilador precisa saber o tamanho de todos os tipos para alocar memória. Tipos recursivos — como listas encadeadas ou árvores — têm tamanho infinito sem Box:

// ERRO: tamanho infinito — Lista contém Lista
enum Lista {
    No(i32, Lista),
    Vazia,
}

// CORRETO: Box tem tamanho fixo (um ponteiro)
#[derive(Debug)]
enum Lista {
    No(i32, Box<Lista>),
    Vazia,
}

fn main() {
    let lista = Lista::No(
        1,
        Box::new(Lista::No(
            2,
            Box::new(Lista::No(
                3,
                Box::new(Lista::Vazia),
            )),
        )),
    );

    println!("{:?}", lista);
}

Caso 2: transferir ownership de dados grandes

Quando você move um valor grande, Rust copia os bytes na stack. Com Box, apenas o ponteiro é copiado — os dados ficam no heap:

fn processar(dados: Box<Vec<f64>>) {
    println!("Processando {} elementos", dados.len());
    // dados destruídos ao final
}

fn main() {
    let grande_dataset = Box::new(vec![0.0f64; 1_000_000]);
    processar(grande_dataset);
    // Apenas o ponteiro foi movido — eficiente
}

Caso 3: trait objects

Como vimos no Artigo #12, Box permite coleções de tipos diferentes:

trait Forma {
    fn area(&self) -> f64;
}

struct Circulo { raio: f64 }
struct Quadrado { lado: f64 }

impl Forma for Circulo {
    fn area(&self) -> f64 { std::f64::consts::PI * self.raio * self.raio }
}

impl Forma for Quadrado {
    fn area(&self) -> f64 { self.lado * self.lado }
}

fn main() {
    let formas: Vec<Box<dyn Forma>> = vec![
        Box::new(Circulo { raio: 3.0 }),
        Box::new(Quadrado { lado: 4.0 }),
        Box::new(Circulo { raio: 1.5 }),
    ];

    let area_total: f64 = formas.iter().map(|f| f.area()).sum();
    println!("Área total: {:.2}", area_total);
}

Deref coercion — a mágica da desreferenciação automática

Box implementa o trait Deref, que permite usá-lo onde T seria esperado. Rust aplica desreferenciação automática em chamadas de método e passagem de argumentos:

fn imprimir_tamanho(s: &str) {
    println!("Tamanho: {}", s.len());
}

fn main() {
    let boxed = Box::new(String::from("hello"));

    // Deref coercion: Box<String> → &String → &str
    imprimir_tamanho(&boxed);

    // Chamadas de método funcionam diretamente
    println!("{}", boxed.to_uppercase());
    println!("{}", boxed.len());
}

Essa cadeia de desreferenciações acontece em tempo de compilação — sem custo em execução.


Rc — múltiplos donos com contagem de referências

O sistema de ownership de Rust permite apenas um dono por valor. Mas às vezes você precisa que múltiplas partes do código compartilhem ownership do mesmo dado. Para isso existe Rc — Reference Counted.

Rc mantém um contador interno. Cada clone incrementa o contador. Quando um Rc é destruído, o contador decrementa. Quando chega a zero, o valor é liberado.

use std::rc::Rc;

fn main() {
    let valor = Rc::new(String::from("compartilhado"));

    println!("Contagem inicial: {}", Rc::strong_count(&valor)); // 1

    let copia1 = Rc::clone(&valor);
    println!("Após copia1: {}", Rc::strong_count(&valor)); // 2

    {
        let copia2 = Rc::clone(&valor);
        println!("Após copia2: {}", Rc::strong_count(&valor)); // 3
        println!("copia2 = {copia2}");
    } // copia2 sai de escopo

    println!("Após destruir copia2: {}", Rc::strong_count(&valor)); // 2
    println!("valor = {valor}");
    println!("copia1 = {copia1}");
} // valor e copia1 saem de escopo — contagem chega a 0 — memória liberada

Note que usamos Rc::clone(&valor) em vez de valor.clone(). Ambos funcionam, mas Rc::clone é convencional — deixa claro que estamos incrementando a contagem, não fazendo uma cópia profunda dos dados.

Rc em estruturas de dados compartilhadas

Um caso de uso clássico é um grafo onde múltiplos nós compartilham referência ao mesmo filho:

use std::rc::Rc;

#[derive(Debug)]
struct No {
    valor: i32,
    filhos: Vec<Rc<No>>,
}

impl No {
    fn novo(valor: i32) -> Rc<Self> {
        Rc::new(No { valor, filhos: Vec::new() })
    }

    fn com_filhos(valor: i32, filhos: Vec<Rc<No>>) -> Rc<Self> {
        Rc::new(No { valor, filhos })
    }
}

fn main() {
    let folha_a = No::novo(10);
    let folha_b = No::novo(20);

    // Dois nós intermediários compartilham folha_a
    let meio1 = No::com_filhos(5, vec![Rc::clone(&folha_a), Rc::clone(&folha_b)]);
    let meio2 = No::com_filhos(7, vec![Rc::clone(&folha_a)]);

    println!("folha_a compartilhada por {} nós",
        Rc::strong_count(&folha_a)); // 3

    let raiz = No::com_filhos(1, vec![meio1, meio2]);
    println!("Raiz: {}", raiz.valor);
    println!("Filhos da raiz: {} nós", raiz.filhos.len());
}

Limitações de Rc

Rc não é thread-safe — só pode ser usado em código single-threaded. Para múltiplos threads, use Arc (Atomic Reference Counted), que veremos no artigo sobre concorrência.

Além disso, Rc só permite acesso imutável aos dados. Para mutabilidade com múltiplos donos, precisamos de RefCell.


RefCell — borrowing verificado em tempo de execução

As regras de borrowing de Rust são verificadas em tempo de compilação na maioria dos casos. Mas há situações onde a verificação estática é impossível — o compilador não consegue provar que as regras são respeitadas, mesmo que você saiba que são.

RefCell adia a verificação de borrowing para tempo de execução. Se as regras forem violadas, o programa entra em panic — mas em código correto, RefCell funciona com segurança:

use std::cell::RefCell;

fn main() {
    let dados = RefCell::new(vec![1, 2, 3]);

    // borrow() retorna uma referência imutável
    {
        let leitura = dados.borrow();
        println!("Dados: {:?}", *leitura);
    } // leitura liberada aqui

    // borrow_mut() retorna uma referência mutável
    {
        let mut escrita = dados.borrow_mut();
        escrita.push(4);
        escrita.push(5);
    } // escrita liberada aqui

    println!("Após modificação: {:?}", dados.borrow());

    // PANIC em tempo de execução — duas referências mutáveis
    // let r1 = dados.borrow_mut();
    // let r2 = dados.borrow_mut(); // panic!
}

borrow() retorna um Ref e borrow_mut() retorna um RefMut — guardas RAII que liberam o empréstimo quando saem de escopo.

O padrão Rc> — mutabilidade com múltiplos donos

A combinação mais poderosa é Rc>: múltiplos donos que podem modificar o valor compartilhado:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Conta {
    titular: String,
    saldo: f64,
}

impl Conta {
    fn nova(titular: &str, saldo: f64) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Conta {
            titular: titular.to_string(),
            saldo,
        }))
    }

    fn depositar(&mut self, valor: f64) {
        self.saldo += valor;
        println!("  Depósito de R${valor:.2} → saldo: R${:.2}", self.saldo);
    }

    fn sacar(&mut self, valor: f64) -> Result<(), String> {
        if valor > self.saldo {
            return Err(format!("Saldo insuficiente: R${:.2}", self.saldo));
        }
        self.saldo -= valor;
        println!("  Saque de R${valor:.2} → saldo: R${:.2}", self.saldo);
        Ok(())
    }
}

fn transferir(
    origem: &Rc<RefCell<Conta>>,
    destino: &Rc<RefCell<Conta>>,
    valor: f64,
) -> Result<(), String> {
    origem.borrow_mut().sacar(valor)?;
    destino.borrow_mut().depositar(valor);
    Ok(())
}

fn main() {
    let conta_ana    = Conta::nova("Ana", 1000.0);
    let conta_carlos = Conta::nova("Carlos", 500.0);

    // Múltiplas referências às mesmas contas
    let referencia_ana1 = Rc::clone(&conta_ana);
    let referencia_ana2 = Rc::clone(&conta_ana);

    println!("── Operações bancárias ──");

    referencia_ana1.borrow_mut().depositar(300.0);
    referencia_ana2.borrow_mut().depositar(200.0);

    println!("
Transferindo R$400 de Ana para Carlos:");
    match transferir(&conta_ana, &conta_carlos, 400.0) {
        Ok(()) => println!("  Transferência realizada"),
        Err(e) => println!("  Erro: {e}"),
    }

    println!("
Tentando transferência impossível:");
    match transferir(&conta_ana, &conta_carlos, 2000.0) {
        Ok(()) => println!("  Transferência realizada"),
        Err(e) => println!("  Erro: {e}"),
    }

    println!("
── Saldos finais ──");
    println!("Ana:    R${:.2}", conta_ana.borrow().saldo);
    println!("Carlos: R${:.2}", conta_carlos.borrow().saldo);
    println!("Ana compartilhada por {} referências",
        Rc::strong_count(&conta_ana));
}

Saída:

── Operações bancárias ──
  Depósito de R$300.00 → saldo: R$1300.00
  Depósito de R$200.00 → saldo: R$1500.00

Transferindo R$400 de Ana para Carlos:
  Saque de R$400.00 → saldo: R$1100.00
  Depósito de R$400.00 → saldo: R$900.00
  Transferência realizada

Tentando transferência impossível:
  Erro: Saldo insuficiente: R$1100.00

── Saldos finais ──
Ana:    R$1100.00
Carlos: R$900.00
Ana compartilhada por 3 referências

Cell — mutabilidade interior para tipos Copy

Para tipos que implementam Copy, Cell oferece mutabilidade interior sem o overhead de RefCell:

use std::cell::Cell;

struct Estatisticas {
    chamadas: Cell<u32>,
    soma: Cell<f64>,
}

impl Estatisticas {
    fn nova() -> Self {
        Estatisticas {
            chamadas: Cell::new(0),
            soma: Cell::new(0.0),
        }
    }

    // &self imutável, mas modifica estado interno
    fn registrar(&self, valor: f64) {
        self.chamadas.set(self.chamadas.get() + 1);
        self.soma.set(self.soma.get() + valor);
    }

    fn media(&self) -> Option<f64> {
        let n = self.chamadas.get();
        if n == 0 { None }
        else { Some(self.soma.get() / n as f64) }
    }

    fn total_chamadas(&self) -> u32 {
        self.chamadas.get()
    }
}

fn main() {
    let stats = Estatisticas::nova();

    for valor in [10.0, 20.0, 30.0, 40.0, 50.0] {
        stats.registrar(valor);
    }

    println!("Chamadas: {}", stats.total_chamadas());
    println!("Média: {:?}", stats.media());
}

Cell não oferece referências — você obtém e define valores inteiros. Por isso só funciona com tipos Copy. É mais eficiente que RefCell para esse caso.


Comparação: quando usar cada smart pointer

// Box<T>
// ✓ Um único dono
// ✓ Dados no heap
// ✓ Tamanho fixo em tempo de compilação
// ✓ Custo zero além da alocação
// Uso: tipos recursivos, trait objects, dados grandes
let b: Box<i32> = Box::new(42);

// Rc<T>
// ✓ Múltiplos donos (single-thread)
// ✓ Contagem de referências automática
// ✗ Apenas acesso imutável
// ✗ Não é thread-safe
// Uso: grafos, estruturas com compartilhamento complexo
use std::rc::Rc;
let r: Rc<i32> = Rc::new(42);

// RefCell<T>
// ✓ Mutabilidade interior
// ✓ Verificação em tempo de execução
// ✗ Panic se regras violadas em execução
// ✗ Não é thread-safe
// Uso: quando compilador é conservador demais, mock objects
use std::cell::RefCell;
let rf: RefCell<i32> = RefCell::new(42);

// Rc<RefCell<T>> — combinação poderosa
// ✓ Múltiplos donos + mutabilidade compartilhada
// ✗ Não é thread-safe, overhead de runtime
// Uso: grafos mutáveis, estado compartilhado em single-thread
use std::rc::Rc;
use std::cell::RefCell;
let shared: Rc<RefCell<i32>> = Rc::new(RefCell::new(42));

Referências cíclicas e Weak

Um problema com Rc é a possibilidade de referências cíclicas — dois valores que se referenciam mutuamente, impedindo que a contagem chegue a zero:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct No {
    valor: i32,
    pai: RefCell<Weak<No>>,      // Weak — não incrementa contagem
    filhos: RefCell<Vec<Rc<No>>>, // Rc — incrementa contagem
}

impl No {
    fn novo(valor: i32) -> Rc<Self> {
        Rc::new(No {
            valor,
            pai: RefCell::new(Weak::new()),
            filhos: RefCell::new(Vec::new()),
        })
    }
}

fn main() {
    let pai = No::novo(1);
    let filho = No::novo(2);

    // Filho conhece o pai via Weak — não cria ciclo
    *filho.pai.borrow_mut() = Rc::downgrade(&pai);

    // Pai conhece o filho via Rc
    pai.filhos.borrow_mut().push(Rc::clone(&filho));

    println!("Pai: {}", pai.valor);
    println!("Filho: {}", filho.valor);
    println!("Contagem do pai: {}", Rc::strong_count(&pai)); // 1
    println!("Contagem do filho: {}", Rc::strong_count(&filho)); // 2

    // Acessando pai a partir do filho
    if let Some(pai_ref) = filho.pai.borrow().upgrade() {
        println!("Pai do filho: {}", pai_ref.valor);
    }
}

Weak é uma referência fraca — não incrementa a contagem forte. Se todos os Rc forem destruídos, o Weak torna-se inválido. upgrade() tenta converter um Weak em Rc — retornando Option>.

A regra prática: use Rc para ownership compartilhado e Weak para referências de "volta" que não devem impedir destruição. Tipicamente: pais possuem filhos com Rc, filhos referenciam pais com Weak.


Um exemplo integrado: sistema de plugins

Vamos usar smart pointers para criar um sistema de plugins extensível:

use std::rc::Rc;
use std::cell::RefCell;

trait Plugin {
    fn nome(&self) -> &str;
    fn executar(&self, entrada: &str) -> String;
}

struct Maiusculo;
struct Reverso;
struct ContadorPalavras;

impl Plugin for Maiusculo {
    fn nome(&self) -> &str { "Maiúsculo" }
    fn executar(&self, entrada: &str) -> String {
        entrada.to_uppercase()
    }
}

impl Plugin for Reverso {
    fn nome(&self) -> &str { "Reverso" }
    fn executar(&self, entrada: &str) -> String {
        entrada.chars().rev().collect()
    }
}

impl Plugin for ContadorPalavras {
    fn nome(&self) -> &str { "ContadorPalavras" }
    fn executar(&self, entrada: &str) -> String {
        let n = entrada.split_whitespace().count();
        format!("{entrada} [{n} palavras]")
    }
}

struct Gerenciador {
    plugins: Vec<Rc<dyn Plugin>>,
    historico: RefCell<Vec<String>>,
}

impl Gerenciador {
    fn novo() -> Self {
        Gerenciador {
            plugins: Vec::new(),
            historico: RefCell::new(Vec::new()),
        }
    }

    fn registrar(&mut self, plugin: Rc<dyn Plugin>) {
        println!("Plugin registrado: {}", plugin.nome());
        self.plugins.push(plugin);
    }

    fn processar(&self, texto: &str) -> Vec<String> {
        let resultados: Vec<String> = self.plugins.iter()
            .map(|p| {
                let resultado = p.executar(texto);
                let entrada = format!("[{}] {}", p.nome(), resultado);
                self.historico.borrow_mut().push(entrada.clone());
                resultado
            })
            .collect();
        resultados
    }

    fn exibir_historico(&self) {
        println!("
── Histórico ──");
        for entrada in self.historico.borrow().iter() {
            println!("  {entrada}");
        }
    }
}

fn main() {
    let mut gerenciador = Gerenciador::novo();

    gerenciador.registrar(Rc::new(Maiusculo));
    gerenciador.registrar(Rc::new(Reverso));
    gerenciador.registrar(Rc::new(ContadorPalavras));

    println!("
── Processando textos ──");
    let textos = ["olá mundo", "rust é incrível", "smart pointers"];

    for texto in &textos {
        println!("
Entrada: '{texto}'");
        let resultados = gerenciador.processar(texto);
        for r in &resultados {
            println!("  → {r}");
        }
    }

    gerenciador.exibir_historico();

    // Plugin compartilhado entre dois gerenciadores
    let plugin_compartilhado = Rc::new(Maiusculo);
    println!("
Plugin usado por {} gerenciadores",
        Rc::strong_count(&plugin_compartilhado));
}

Saída:

Plugin registrado: Maiúsculo
Plugin registrado: Reverso
Plugin registrado: ContadorPalavras

── Processando textos ──

Entrada: 'olá mundo'
  → OLÁ MUNDO
  → odnum álo
  → olá mundo [2 palavras]

Entrada: 'rust é incrível'
  → RUST É INCRÍVEL
  → lévírcni é tsur
  → rust é incrível [3 palavras]

Entrada: 'smart pointers'
  → SMART POINTERS
  → sretnop trams
  → smart pointers [2 palavras]

── Histórico ──
  [Maiúsculo] OLÁ MUNDO
  [Reverso] odnum álo
  [ContadorPalavras] olá mundo [2 palavras]
  ...

Resumo mental dos smart pointers

Smart pointers existem para resolver problemas específicos que referências simples não conseguem resolver. Use a ferramenta mínima necessária:

Se o problema é alocação no heap ou tipo de tamanho desconhecido, use Box.

Se o problema é múltiplos donos em código single-threaded, use Rc.

Se o problema é mutabilidade interior — modificar através de referência imutável — use RefCell ou Cell.

Se você precisa de múltiplos donos E mutabilidade compartilhada, combine Rc>.

Se precisa de referências que não impeçam destruição, use Weak.

E quando chegar ao artigo sobre concorrência, vai aprender Arc e Mutex — as versões thread-safe de Rc e RefCell.


Fontes e leituras recomendadas

  • The Rust Programming Language, Cap. 15Smart Pointers — https://doc.rust-lang.org/book/ch15-00-smart-pointers.html
  • Rust by Example — Box, stack and heap — https://doc.rust-lang.org/rust-by-example/std/box.html
  • Rust Standard Library — std::rc — documentação completa de Rc e Weak — https://doc.rust-lang.org/std/rc/index.html
  • Rust Standard Library — std::cell — RefCell e Cell — https://doc.rust-lang.org/std/cell/index.html
  • "Too Many Linked Lists" — Learn Rust With Entirely Too Many Linked Lists — guia prático e divertido de smart pointers — https://rust-unofficial.github.io/too-many-lists/
  • "Interior Mutability in Rust" — Manish Goregaokar — artigo aprofundado — https://ricardomartins.cc/2016/06/08/interior-mutability
Comentários

Mais em Rust

Borrowing e Referências — Usando sem Possuir
Borrowing e Referências — Usando sem Possuir

&nbsp; No artigo anterior, aprendemos que ownership resolve o problema do ge...

Coleções — Vec, HashMap e HashSet na Prática
Coleções — Vec, HashMap e HashSet na Prática

&nbsp; At&eacute; agora trabalhamos com dados de tamanho fixo &mdash; arrays...

Tratamento de Erros com Result — Erros como Valores, não Exceções
Tratamento de Erros com Result — Erros como Valores, não Exceções

&nbsp; Em linguagens como Java, Python e C#, erros s&atilde;o tratados com e...