Rc<T> e Arc<T>: Contagem de Referências

Guia completo de Rc<T> e Arc<T> em Rust: ownership compartilhado, referências fracas, Arc+Mutex para threads e padrões práticos.

O que são Rc<T> e Arc<T>

Rc<T> (Reference Counted) e Arc<T> (Atomically Reference Counted) são smart pointers que permitem múltiplos donos para o mesmo valor. Cada vez que você clona um Rc ou Arc, apenas o contador de referências é incrementado — os dados não são copiados. Quando o último dono é liberado (o contador chega a zero), a memória é desalocada automaticamente.

A diferença crucial entre eles é: Rc<T> funciona apenas em single-thread (não implementa Send nem Sync), enquanto Arc<T> usa operações atômicas no contador, tornando-o seguro para uso entre threads. O Arc tem um pequeno custo extra de performance por causa da sincronização atômica. Use Rc quando possível e Arc apenas quando precisar compartilhar dados entre threads.


Criando Rc e Arc

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // Rc<T> — single thread
    let rc1 = Rc::new(String::from("compartilhado"));
    let rc2 = Rc::clone(&rc1); // Incrementa contador, NÃO clona a String
    let rc3 = rc1.clone();     // Equivalente a Rc::clone

    println!("Valor: {rc1}");
    println!("Referências: {}", Rc::strong_count(&rc1)); // 3

    drop(rc3);
    println!("Após drop: {}", Rc::strong_count(&rc1)); // 2

    // Arc<T> — thread-safe
    let arc1 = Arc::new(vec![1, 2, 3]);
    let arc2 = Arc::clone(&arc1);

    println!("Arc refs: {}", Arc::strong_count(&arc1)); // 2
    println!("Dados: {arc2:?}");
}

Tabela de Métodos Principais

Rc<T>

MétodoDescriçãoExemplo
Rc::new(val)Cria um novo RcRc::new(42)
Rc::clone(&rc)Incrementa a contagem (sem cópia de dados)Rc::clone(&meu_rc)
Rc::strong_count(&rc)Número de referências fortesRc::strong_count(&rc)3
Rc::weak_count(&rc)Número de referências fracasRc::weak_count(&rc)1
Rc::downgrade(&rc)Cria uma referência fraca (Weak<T>)Rc::downgrade(&rc)
Rc::try_unwrap(rc)Extrai o valor se há exatamente 1 refRc::try_unwrap(rc).ok()
Rc::into_inner(rc)Extrai se é o último dono (Rust 1.70+)Rc::into_inner(rc)
Rc::ptr_eq(&a, &b)Compara ponteiros (mesma alocação?)Rc::ptr_eq(&rc1, &rc2)

Arc<T>

MétodoDescriçãoExemplo
Arc::new(val)Cria um novo ArcArc::new(42)
Arc::clone(&arc)Incrementa a contagem atomicamenteArc::clone(&meu_arc)
Arc::strong_count(&arc)Número de referências fortesArc::strong_count(&arc)
Arc::weak_count(&arc)Número de referências fracasArc::weak_count(&arc)
Arc::downgrade(&arc)Cria Weak<T>Arc::downgrade(&arc)
Arc::try_unwrap(arc)Extrai se há exatamente 1 refArc::try_unwrap(arc).ok()
Arc::ptr_eq(&a, &b)Compara ponteirosArc::ptr_eq(&a1, &a2)

Weak<T>

MétodoDescriçãoExemplo
weak.upgrade()Tenta obter Option<Rc<T>> ou Option<Arc<T>>weak.upgrade()
weak.strong_count()Contagem de refs fortesweak.strong_count()

Exemplos Práticos

1. Compartilhando Dados entre Estruturas

use std::rc::Rc;

#[derive(Debug)]
struct Cidade {
    nome: String,
    populacao: u64,
}

#[derive(Debug)]
struct Empresa {
    nome: String,
    sede: Rc<Cidade>,
}

#[derive(Debug)]
struct Universidade {
    nome: String,
    cidade: Rc<Cidade>,
}

fn main() {
    let sp = Rc::new(Cidade {
        nome: "São Paulo".into(),
        populacao: 12_300_000,
    });

    let empresa = Empresa {
        nome: "TechBR".into(),
        sede: Rc::clone(&sp),
    };

    let uni = Universidade {
        nome: "USP".into(),
        cidade: Rc::clone(&sp),
    };

    println!("{} tem sede em {}", empresa.nome, empresa.sede.nome);
    println!("{} fica em {}", uni.nome, uni.cidade.nome);
    println!("Referências a SP: {}", Rc::strong_count(&sp)); // 3

    // Comparação de ponteiros — são o MESMO objeto
    assert!(Rc::ptr_eq(&empresa.sede, &uni.cidade));
}

2. Referências Fracas para Evitar Ciclos

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

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

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

    fn adicionar_filho(pai: &Rc<No>, filho: &Rc<No>) {
        pai.filhos.borrow_mut().push(Rc::clone(filho));
        *filho.pai.borrow_mut() = Rc::downgrade(pai);
    }
}

fn main() {
    let raiz = No::novo(1);
    let filho_a = No::novo(2);
    let filho_b = No::novo(3);

    No::adicionar_filho(&raiz, &filho_a);
    No::adicionar_filho(&raiz, &filho_b);

    // Navegar de filho para pai
    if let Some(pai) = filho_a.pai.borrow().upgrade() {
        println!("Pai do nó {}: {}", filho_a.valor, pai.valor); // Pai do nó 2: 1
    }

    println!("Raiz refs fortes: {}", Rc::strong_count(&raiz)); // 1
    println!("Raiz refs fracas: {}", Rc::weak_count(&raiz));   // 2

    // Sem Weak, haveria um ciclo pai→filho→pai que impediria a desalocação
}

3. Arc + Mutex para Dados Compartilhados entre Threads

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let contador = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..10 {
        let contador = Arc::clone(&contador);
        let handle = thread::spawn(move || {
            let mut num = contador.lock().unwrap();
            *num += 1;
            println!("Thread {i}: contador = {num}");
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Resultado final: {}", *contador.lock().unwrap()); // 10
}

4. Cache Compartilhado com Arc + RwLock

use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::thread;

type Cache = Arc<RwLock<HashMap<String, String>>>;

fn buscar_com_cache(cache: &Cache, chave: &str) -> String {
    // Tenta ler primeiro (múltiplas threads podem ler simultaneamente)
    {
        let leitura = cache.read().unwrap();
        if let Some(valor) = leitura.get(chave) {
            return valor.clone();
        }
    } // Lock de leitura liberado aqui

    // Se não encontrou, simula busca e salva no cache
    let valor = format!("resultado_para_{chave}");
    {
        let mut escrita = cache.write().unwrap();
        escrita.insert(chave.to_string(), valor.clone());
    }
    valor
}

fn main() {
    let cache: Cache = Arc::new(RwLock::new(HashMap::new()));
    let mut handles = vec![];

    let chaves = vec!["user:1", "user:2", "user:1", "user:3", "user:2"];

    for chave in chaves {
        let cache = Arc::clone(&cache);
        let chave = chave.to_string();
        let handle = thread::spawn(move || {
            let resultado = buscar_com_cache(&cache, &chave);
            println!("[{chave}] → {resultado}");
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    let leitura = cache.read().unwrap();
    println!("\nCache final: {} entradas", leitura.len());
    for (k, v) in leitura.iter() {
        println!("  {k}: {v}");
    }
}

5. Padrão Observer com Rc e Weak

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

trait Observador {
    fn notificar(&self, mensagem: &str);
}

struct EventBus {
    ouvintes: RefCell<Vec<Weak<dyn Observador>>>,
}

impl EventBus {
    fn novo() -> Self {
        EventBus {
            ouvintes: RefCell::new(Vec::new()),
        }
    }

    fn registrar(&self, ouvinte: &Rc<dyn Observador>) {
        self.ouvintes.borrow_mut().push(Rc::downgrade(ouvinte));
    }

    fn emitir(&self, mensagem: &str) {
        let mut ouvintes = self.ouvintes.borrow_mut();
        // Remove ouvintes que já foram desalocados e notifica os ativos
        ouvintes.retain(|weak| {
            if let Some(ouvinte) = weak.upgrade() {
                ouvinte.notificar(mensagem);
                true
            } else {
                false // Ouvinte foi liberado, remover da lista
            }
        });
    }
}

struct Logger {
    prefixo: String,
}

impl Observador for Logger {
    fn notificar(&self, mensagem: &str) {
        println!("[{}] {mensagem}", self.prefixo);
    }
}

fn main() {
    let bus = EventBus::novo();

    let logger1: Rc<dyn Observador> = Rc::new(Logger {
        prefixo: "INFO".into(),
    });
    let logger2: Rc<dyn Observador> = Rc::new(Logger {
        prefixo: "DEBUG".into(),
    });

    bus.registrar(&logger1);
    bus.registrar(&logger2);

    bus.emitir("Sistema iniciado");
    // [INFO] Sistema iniciado
    // [DEBUG] Sistema iniciado

    drop(logger2); // Remove o logger DEBUG

    bus.emitir("Processando requisição");
    // [INFO] Processando requisição
    // (logger2 foi automaticamente removido)
}

Dicas de Performance e Armadilhas

  1. Rc não é thread-safe: Tentar enviar um Rc entre threads gera erro de compilação. Use Arc para cenários multi-thread.

  2. Rc/Arc são imutáveis por padrão: Para mutabilidade interna, combine com Cell, RefCell (single-thread) ou Mutex, RwLock (multi-thread).

  3. Ciclos de referência: Rc e Arc não detectam ciclos. Se A aponta para B e B aponta para A, a memória nunca é liberada. Use Weak para quebrar ciclos.

  4. Clone é barato, mas não gratuito: Rc::clone e Arc::clone incrementam um contador. Para Arc, é uma operação atômica — mais cara que Rc mas ainda muito rápida.

  5. Prefira Rc::clone(&rc) a rc.clone(): A forma Rc::clone(&rc) deixa claro que estamos incrementando a contagem, não clonando os dados. É uma convenção importante em Rust.

  6. Arc<Mutex<T>> é o padrão: Para dados compartilhados e mutáveis entre threads, Arc<Mutex<T>> é o padrão mais comum. Para leituras frequentes, considere Arc<RwLock<T>>.


Veja Também