Em Rust, o modelo de ownership garante segurança de memória, mas torna desafiador compartilhar dados entre múltiplos donos. O tipo Rc<T> (Reference Counted) resolve esse problema permitindo que um valor tenha vários proprietários, usando contagem de referências em tempo de execução. Nesta aula, você aprenderá como usar Rc<T>, suas vantagens, limitações e padrões de uso.

Rc<T> é um smart pointer que mantém um contador interno de quantas referências ativas existem para o valor. Quando o contador chega a zero, o valor é liberado. Ele é útil em estruturas de dados como grafos ou árvores onde um nó pode ser referenciado por múltiplos pais.

Compartilhamento de ownership

Em Rust, cada valor tem exatamente um dono. Para compartilhar um valor entre várias partes, normalmente precisaríamos de referências emprestadas (&T), mas elas têm lifetimes que nem sempre são adequados. Rc<T> permite que múltiplos donos existam simultaneamente, cada um mantendo uma referência de contagem.

Para criar um Rc, use Rc::new(valor). Para obter uma nova referência compartilhada, chame Rc::clone(&rc). Isso incrementa o contador interno. O valor só é destruído quando todas as referências são descartadas.

use std::rc::Rc;

let a = Rc::new(5);
let b = Rc::clone(&a);
let c = Rc::clone(&a);

println!("a: {}, b: {}, c: {}", a, b, c); // todos apontam para o mesmo valor 5
println!("contagem de referências: {}", Rc::strong_count(&a)); // 3

No exemplo, a, b e c compartilham o mesmo inteiro. O contador é 3. Quando c sai de escopo, o contador diminui para 2. O valor só é liberado quando a e b também saírem.

Rc::clone

Rc::clone é o método para criar uma nova referência. Diferente de uma clonagem profunda (clone em tipos como String), Rc::clone é superficial: apenas incrementa o contador e copia o ponteiro. É muito eficiente (custo O(1)).

É importante notar que Rc implementa o trait Clone, então você pode chamar .clone() diretamente, que é equivalente a Rc::clone. Porém, a forma explícita Rc::clone(&rc) é preferida na comunidade para deixar claro que não é uma clonagem profunda.

use std::rc::Rc;

let original = Rc::new(vec![1, 2, 3]);
let ref1 = original.clone(); // também funciona
let ref2 = Rc::clone(&original); // forma explícita

println!("strong_count: {}", Rc::strong_count(&original)); // 3

Lembre-se: Rc::clone não clona o valor interno (o vetor), apenas incrementa o contador. O vetor permanece alocado uma vez.

Limitações

Apesar de útil, Rc<T> tem limitações importantes:

  • Apenas para uso single-thread: Rc<T> não implementa Send nem Sync, portanto não pode ser transferido entre threads. Para concorrência, use Arc<T> (Atomic Reference Counted).
  • Imutabilidade compartilhada: O valor dentro de Rc<T> é imutável (a menos que combinado com RefCell<T>). Se você precisa de mutabilidade compartilhada, use Rc<RefCell<T>>.
  • Possibilidade de referências cíclicas: Se duas Rc apontam uma para a outra, o contador nunca chega a zero, causando vazamento de memória. Para evitar isso, use Weak<T>.
use std::rc::Rc;

let x = Rc::new(42);
// let y = *x; // erro: não é possível mover o valor para fora de Rc

Para modificar o valor internamente, você pode usar Rc::make_mut se houver apenas uma referência forte, ou Rc<RefCell<T>>.

Casos de uso

Rc<T> é ideal para estruturas de dados onde múltiplos proprietários são necessários, como:

  • Árvores: Um nó pode ter vários filhos, e cada filho pode ter um pai. Usando Rc, os filhos podem ser compartilhados entre pais.
  • Grafos: Nós de um grafo podem ser referenciados por múltiplos arestas.
  • Cache: Um valor pode ser mantido em cache e referenciado por vários consumidores.
  • Implementação de tipos como LinkedList: Onde um nó pode pertencer a múltiplas listas.
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

fn main() {
    let leaf = Rc::new(Node { value: 3, children: vec![] });
    let branch1 = Rc::new(Node { value: 5, children: vec![Rc::clone(&leaf)] });
    let branch2 = Rc::new(Node { value: 7, children: vec![Rc::clone(&leaf)] });
    // leaf é compartilhado por branch1 e branch2
}

Nesse exemplo, a folha leaf é compartilhada por dois ramos. O contador de referências de leaf é 3 (um da variável leaf e dois das children). Quando branch1 e branch2 saem de escopo, o contador diminui, e leaf só é liberado quando a variável leaf também sair.

Boas práticas

Use Rc quando precisar de compartilhamento de ownership dentro de um único thread. Prefira Arc para concorrência. Evite referências cíclicas usando Weak para referências que não devem manter o valor vivo. Considere Rc<RefCell<T>> para mutabilidade interior.

Referências

Exercícios

  1. Crie um programa que use Rc<String> para compartilhar uma string entre três variáveis. Imprima a contagem de referências após cada clone.

    ✓ Resposta:
    use std::rc::Rc;
    
    fn main() {
        let s = Rc::new("Hello".to_string());
        println!("Contagem inicial: {}", Rc::strong_count(&s));
        let t = Rc::clone(&s);
        println!("Após primeiro clone: {}", Rc::strong_count(&s));
        let u = Rc::clone(&s);
        println!("Após segundo clone: {}", Rc::strong_count(&s));
        println!("s: {}, t: {}, u: {}", s, t, u);
    }
    
  2. Explique por que Rc::clone é eficiente comparado a clonar o valor interno.

    ✓ Resposta: Rc::clone apenas incrementa o contador de referências e copia o ponteiro, sem alocar nova memória ou copiar o valor interno. É uma operação O(1), enquanto clonar o valor (ex.: String::clone) pode ser O(n) e alocar nova memória.
  3. O que acontece se você tentar usar Rc em um programa multithread? Como corrigir?

    ✓ Resposta: Rc não implementa Send e Sync, então o compilador impedirá seu uso em threads. Para compartilhamento entre threads, use Arc<T> (Atomic Reference Counted), que usa contagem atômica.
  4. Crie um exemplo de referência cíclica com Rc que cause vazamento de memória. Sugira uma solução.

    ✓ Resposta:
    use std::rc::Rc;
    use std::cell::RefCell;
    
    #[derive(Debug)]
    struct Node {
        value: i32,
        next: Option<Rc<RefCell<Node>>>,
    }
    
    fn main() {
        let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
        let b = Rc::new(RefCell::new(Node { value: 2, next: Some(Rc::clone(&a)) }));
        // Cria ciclo: a.next = Some(b)
        a.borrow_mut().next = Some(Rc::clone(&b));
        // Agora a e b se referem mutuamente, contadores nunca zeram.
        // Solução: usar Weak para uma das referências.
    }
    
  5. Implemente uma árvore binária simples onde nós podem ter múltiplos pais usando Rc. Mostre como compartilhar um nó filho.

    ✓ Resposta:
    use std::rc::Rc;
    
    #[derive(Debug)]
    struct TreeNode {
        value: i32,
        left: Option<Rc<TreeNode>>,
        right: Option<Rc<TreeNode>>,
    }
    
    fn main() {
        let leaf = Rc::new(TreeNode { value: 3, left: None, right: None });
        let branch1 = Rc::new(TreeNode { value: 5, left: Some(Rc::clone(&leaf)), right: None });
        let branch2 = Rc::new(TreeNode { value: 7, left: Some(Rc::clone(&leaf)), right: None });
        println!("branch1: {:?}", branch1);
        println!("branch2: {:?}", branch2);
    }