Rc<T>: contagem de referências
Esta aula explora Rc<T>, um smart pointer de Rust que permite compartilhamento de ownership via contagem de referências. Aborda como Rc::clone funciona, suas limitações (apenas thread-local, sem mutabilidade compartilhada) e casos de uso típicos como grafos e árvores.
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 implementaSendnemSync, portanto não pode ser transferido entre threads. Para concorrência, useArc<T>(Atomic Reference Counted). - Imutabilidade compartilhada: O valor dentro de
Rc<T>é imutável (a menos que combinado comRefCell<T>). Se você precisa de mutabilidade compartilhada, useRc<RefCell<T>>. - Possibilidade de referências cíclicas: Se duas
Rcapontam uma para a outra, o contador nunca chega a zero, causando vazamento de memória. Para evitar isso, useWeak<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
- Documentação oficial de Rc
- The Rust Book: Rc<T>
- Rust by Example: Rc
- The Rustonomicon: Arc and Mutex
- Documentação de Weak
Exercícios
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); }Explique por que
Rc::cloneé eficiente comparado a clonar o valor interno.✓ Resposta:Rc::cloneapenas 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.O que acontece se você tentar usar
Rcem um programa multithread? Como corrigir?✓ Resposta:Rcnão implementaSendeSync, então o compilador impedirá seu uso em threads. Para compartilhamento entre threads, useArc<T>(Atomic Reference Counted), que usa contagem atômica.Crie um exemplo de referência cíclica com
Rcque 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. }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); }