RefCell e mutabilidade interior
Nesta aula, exploramos o padrão de mutabilidade interior em Rust usando RefCell<T>, que permite mutabilidade mesmo com referências imutáveis, transferindo a verificação de borrows para o runtime. Aprendemos a usar borrow e borrow_mut, combinamos Rc<RefCell<T>> para ter múltiplas referências mutáveis, e discutimos os riscos como panics em runtime e possíveis vazamentos de memória.
Até agora, seguimos à risca as regras de ownership do Rust: ou temos uma referência imutável (&T) ou uma referência mutável (&mut T), mas nunca ambas ao mesmo tempo. Essas verificações são feitas em tempo de compilação, garantindo segurança de memória sem custo em runtime. No entanto, há situações em que precisamos de mutabilidade mesmo quando o valor é referenciado de forma imutável – por exemplo, em estruturas de dados como listas ligadas ou implementações de cache. Para esses casos, o Rust oferece o padrão de mutabilidade interior (interior mutability), implementado por tipos como RefCell<T>.
RefCell<T> é um tipo smart pointer que permite que você mude o valor interno mesmo quando o RefCell é imutável. Em vez de verificar as regras de borrow em tempo de compilação, o RefCell as verifica em tempo de execução (runtime). Isso dá mais flexibilidade, mas com o custo de uma pequena penalidade de desempenho e o risco de causar um panic se as regras forem violadas. Nesta aula, vamos entender como usar borrow e borrow_mut, como combinar RefCell com Rc para ter múltiplos proprietários mutáveis, e quais cuidados tomar.
borrow e borrow_mut
Para acessar o valor dentro de um RefCell<T>, usamos os métodos borrow() e borrow_mut(). borrow() retorna uma referência imutável (&T) ao valor interno, enquanto borrow_mut() retorna uma referência mutável (&mut T). A grande diferença é que essas chamadas são verificadas em runtime: se você tentar obter uma referência mutável enquanto já existir uma referência imutável ativa (ou vice-versa), o programa entra em panic.
Esses métodos retornam wrappers inteligentes chamados Ref e RefMut, que implementam Deref e Drop. Quando um Ref ou RefMut sai de escopo, ele libera o empréstimo, permitindo que novos empréstimos ocorram. É importante não armazenar essas referências por mais tempo do que o necessário, pois enquanto elas existirem, outros empréstimos podem ser bloqueados.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
// borrow imutável
let borrowed = data.borrow();
println!("Valor: {}", *borrowed);
// borrowed sai de escopo aqui
// borrow mutável
let mut borrowed_mut = data.borrow_mut();
*borrowed_mut += 10;
println!("Valor após mutação: {}", *borrowed_mut);
// borrowed_mut sai de escopo
// Agora podemos fazer outro borrow imutável
let borrowed2 = data.borrow();
println!("Valor final: {}", *borrowed2);
}
Checagem em runtime
A principal característica do RefCell é que a verificação das regras de borrow ocorre em tempo de execução. Isso significa que o código compila mesmo se você violar as regras, mas vai panicar ao executar. Por exemplo, o código abaixo compila perfeitamente, mas causa um panic porque tentamos obter um borrow mutável enquanto um borrow imutável ainda está ativo:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
let ref1 = data.borrow(); // borrow imutável
let ref2 = data.borrow(); // outro borrow imutável (OK)
let mut ref_mut = data.borrow_mut(); // PANIC! já existem borrows imutáveis
// Esta linha nunca será executada
println!("{} {}", ref1, ref2);
}
Ao executar, você verá uma mensagem como: already borrowed: BorrowMutError. Isso é um panic e, a menos que você tenha um catch_unwind, o programa será abortado. Essa verificação é feita mantendo um contador de borrows ativos (como um semáforo) dentro do RefCell. A vantagem é que você pode escrever código que seria ilegal para o borrow checker do compilador, mas precisa garantir que, em tempo de execução, os borrows não se sobreponham indevidamente.
Rc<RefCell<T>>
Combinar Rc<T> (contagem de referências) com RefCell<T> é uma técnica poderosa para ter múltiplos proprietários de um valor mutável. Rc permite que múltiplas partes do código compartilhem a propriedade (ownership) de um valor, mas por padrão ele só dá acesso imutável. Envolvendo o valor em RefCell, podemos obter mutabilidade interior através de referências Rc imutáveis.
Isso é muito útil para implementar estruturas de dados como grafos, onde um nó pode ser referenciado por vários outros nós, e você precisa modificar o nó a partir de qualquer referência. Exemplo:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct No {
valor: i32,
vizinhos: Vec<Rc<RefCell<No>>>,
}
fn main() {
let a = Rc::new(RefCell::new(No { valor: 1, vizinhos: vec![] }));
let b = Rc::new(RefCell::new(No { valor: 2, vizinhos: vec![] }));
// a e b são vizinhos mútuos
a.borrow_mut().vizinhos.push(Rc::clone(&b));
b.borrow_mut().vizinhos.push(Rc::clone(&a));
// Modificando o valor de a através de b
b.borrow_mut().vizinhos[0].borrow_mut().valor = 10;
println!("a: {:?}", a.borrow());
println!("b: {:?}", b.borrow());
}
Observe que usamos Rc::clone para incrementar a contagem de referências, e borrow_mut para modificar o valor interno. Isso permite que múltiplos nós compartilhem e modifiquem uns aos outros de forma segura (em runtime).
Riscos
Embora RefCell seja muito útil, ele introduz riscos que não existem com o borrow checker estático. O principal é o panic em runtime devido a violações das regras de borrow. Se você não tomar cuidado, pode facilmente criar situações onde dois borrows mutáveis ou um borrow mutável com um imutável coexistem, resultando em um crash inesperado. Isso é especialmente perigoso em programas que devem ser robustos, como servidores ou sistemas embarcados.
Outro risco é a possibilidade de vazamento de memória quando combinado com Rc. Se você criar referências cíclicas entre Rcs que contêm RefCell, a contagem de referências nunca chegará a zero, e a memória nunca será liberada. Diferente de Arc (que suporta multithreading), Rc não é thread-safe, então você não pode usar RefCell em múltiplas threads sem proteção adicional (como Mutex).
Além disso, o uso excessivo de RefCell pode tornar o código mais difícil de entender e depurar, pois as regras de empréstimo não são mais verificadas em tempo de compilação. Prefira sempre o borrow checker estático quando possível, e use RefCell apenas quando houver uma necessidade clara de mutabilidade interior.
Boas práticas
Para minimizar os riscos, siga estas recomendações:
- Mantenha os borrows ativos pelo menor tempo possível. Use blocos ou funções para limitar o escopo das referências retornadas por
borrow()eborrow_mut(). - Evite armazenar
RefouRefMutem estruturas de dados que possam ser acessadas em ordens imprevisíveis. - Considere usar
Cell<T>para tiposCopyse você não precisar de referências para o valor –Cellé mais leve e não tem verificação em runtime. - Em cenários multithread, use
Arc<Mutex<T>>em vez deRc<RefCell<T>>.
Referências
- Documentação oficial do RefCell
- The Rust Book: Interior Mutability
- Rust by Example: Rc
- std::cell – Módulo de células
- The Rustonomicon: Borrow Splitting
- Documentação do Rc
Exercícios
-
Crie um
RefCell<i32>com valor 100. Em seguida, obtenha um borrow imutável e imprima o valor. Depois, obtenha um borrow mutável e adicione 50. Por fim, imprima o valor final usando outro borrow imutável.✓ Resposta:use std::cell::RefCell; fn main() { let data = RefCell::new(100); { let x = data.borrow(); println!("Valor inicial: {}", *x); } { let mut y = data.borrow_mut(); *y += 50; } { let z = data.borrow(); println!("Valor final: {}", *z); } } -
Escreva um código que cause um panic ao tentar ter dois borrows mutáveis ativos ao mesmo tempo em um
RefCell. Explique a mensagem de erro.✓ Resposta:use std::cell::RefCell; fn main() { let cell = RefCell::new(0); let mut a = cell.borrow_mut(); let mut b = cell.borrow_mut(); // PANIC: already borrowed: BorrowMutError *a = 1; *b = 2; }Ao executar, o programa panicará com a mensagem
already borrowed: BorrowMutErrorporque o segundo borrow mutável é tentado enquanto o primeiro ainda está ativo. -
Implemente uma estrutura
Contadorque useRc<RefCell<u32>>e forneça um métodoincrementarque adiciona 1 ao valor interno. Crie duas referênciasRcpara o mesmo contador e chameincrementaratravés de cada uma.✓ Resposta:use std::rc::Rc; use std::cell::RefCell; struct Contador(Rc<RefCell<u32>>); impl Contador { fn new() -> Self { Contador(Rc::new(RefCell::new(0))) } fn incrementar(&self) { *self.0.borrow_mut() += 1; } fn valor(&self) -> u32 { *self.0.borrow() } fn clone_ref(&self) -> Self { Contador(Rc::clone(&self.0)) } } fn main() { let c1 = Contador::new(); let c2 = c1.clone_ref(); c1.incrementar(); c2.incrementar(); println!("Valor final: {}", c1.valor()); // 2 } -
Explique por que o código abaixo compila mas pode panicar em runtime. Qual é a condição que causa o panic?
use std::cell::RefCell; fn main() { let x = RefCell::new(vec![1,2,3]); let a = x.borrow(); let b = x.borrow_mut(); println!("{:?}", *a); }✓ Resposta:O código compila porque as regras de borrow não são verificadas em tempo de compilação para
RefCell. Em runtime, ele panicará porquebé um borrow mutável enquantoa(um borrow imutável) ainda está ativo. A condição que causa o panic é ter um borrow mutável e um imutável simultaneamente ativos. Para evitar o panic, poderíamos garantir queaseja dropado antes de criarb, por exemplo, colocandoaem um escopo interno. -
Crie um grafo simples com dois nós (
Rc<RefCell<No>>) onde cada nó tem um camponome(String) e uma lista de vizinhos. Faça com que o nó A aponte para B e B aponte para A. Depois, modifique o nome do nó A através do nó B.✓ Resposta:use std::rc::Rc; use std::cell::RefCell; #[derive(Debug)] struct No { nome: String, vizinhos: Vec<Rc<RefCell<No>>>, } fn main() { let a = Rc::new(RefCell::new(No { nome: "A".to_string(), vizinhos: vec![] })); let b = Rc::new(RefCell::new(No { nome: "B".to_string(), vizinhos: vec![] })); a.borrow_mut().vizinhos.push(Rc::clone(&b)); b.borrow_mut().vizinhos.push(Rc::clone(&a)); // Modifica o nome de A através de B { let b_ref = b.borrow(); let a_vizinho = &b_ref.vizinhos[0]; a_vizinho.borrow_mut().nome = "Modificado".to_string(); } println!("A: {:?}", a.borrow()); println!("B: {:?}", b.borrow()); }