RefCell e interior mutability
1. O Padrão Interior Mutability em Rust
O padrão interior mutability (mutabilidade interior) é uma das características mais poderosas e controversas do sistema de tipos de Rust. Ele permite que você mute dados através de referências imutáveis, contornando as regras de borrowing que o compilador normalmente impõe em tempo de compilação.
A motivação é simples: existem situações onde você precisa de mutabilidade, mas as regras do borrow checker impedem. Por exemplo, quando você tem um valor compartilhado por múltiplas referências imutáveis (via Rc<T>) e precisa modificá-lo.
Em Rust, a mutabilidade é verificada em tempo de compilação (compile-time). O padrão interior mutability move essa verificação para tempo de execução (runtime), trocando segurança estática por flexibilidade dinâmica.
Cenários reais onde isso é necessário:
- Implementação de caches que precisam ser atualizados mesmo com referências imutáveis
- Mocks em testes que precisam registrar chamadas
- Estruturas de dados como grafos com referências compartilhadas
2. Entendendo o RefCell
RefCell<T> é um tipo de smart pointer que implementa o padrão interior mutability. Diferente de Box<T>, que segue as regras de borrowing estritamente em tempo de compilação, RefCell<T> as aplica em tempo de execução.
use std::cell::RefCell;
fn main() {
let valor = RefCell::new(42);
// Borrow imutável em tempo de execução
let ref1 = valor.borrow();
let ref2 = valor.borrow(); // Múltiplos borrows imutáveis são permitidos
println!("{} {}", ref1, ref2);
// Borrow mutável
*valor.borrow_mut() += 1;
println!("{}", valor.borrow());
}
A diferença fundamental:
- Box<T>: ownership único, mutabilidade verificada em compile-time
- Rc<T>: múltiplos owners, mas apenas leitura compartilhada
- RefCell<T>: mutabilidade interior, verificada em runtime
3. Métodos Principais do RefCell
Os métodos mais importantes são borrow() e borrow_mut():
use std::cell::RefCell;
let cell = RefCell::new(String::from("Hello"));
// borrow() retorna Ref<T>, uma referência imutável
let ref_imutavel = cell.borrow();
println!("{}", ref_imutavel);
// borrow_mut() retorna RefMut<T>, uma referência mutável
let mut ref_mutavel = cell.borrow_mut();
ref_mutavel.push_str(", World!");
println!("{}", ref_mutavel);
As versões try_borrow() e try_borrow_mut() retornam Result:
let cell = RefCell::new(10);
let ref1 = cell.borrow();
let resultado = cell.try_borrow_mut(); // Retorna Err porque já há um borrow ativo
match resultado {
Ok(_) => println!("Conseguiu borrow mutável"),
Err(e) => println!("Erro: {}", e),
}
Métodos auxiliares úteis:
let mut cell = RefCell::new(5);
// into_inner() consome o RefCell e retorna o valor interno
let valor = cell.into_inner();
println!("{}", valor); // 5
// get_mut() retorna &mut T, mas requer &mut self
let mut cell = RefCell::new(10);
*cell.get_mut() = 20;
// replace() substitui o valor e retorna o antigo
let cell = RefCell::new(100);
let antigo = cell.replace(200);
println!("Antigo: {}, Novo: {}", antigo, cell.borrow());
4. Combinando RefCell com Rc
A combinação Rc<RefCell<T>> é extremamente poderosa: permite múltiplos owners com mutabilidade compartilhada.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct No {
valor: i32,
vizinhos: Vec<Rc<RefCell<No>>>,
}
fn main() {
let no_a = Rc::new(RefCell::new(No {
valor: 1,
vizinhos: vec![],
}));
let no_b = Rc::new(RefCell::new(No {
valor: 2,
vizinhos: vec![Rc::clone(&no_a)],
}));
// Adiciona referência circular
no_a.borrow_mut().vizinhos.push(Rc::clone(&no_b));
// Modifica através de qualquer referência
no_b.borrow_mut().valor = 42;
println!("{:?}", no_a.borrow());
}
Cuidado com ciclos de referência: referências circulares com Rc podem causar vazamentos de memória. Use Weak<T> para quebrar ciclos.
5. Borrow Checker em Tempo de Execução
O RefCell<T> mantém um contador interno de referências ativas:
- Múltiplos borrow() são permitidos simultaneamente
- Apenas um borrow_mut() por vez
- borrow() e borrow_mut() não podem coexistir
Violar essas regras causa panic em tempo de execução:
let cell = RefCell::new(0);
let ref_mut = cell.borrow_mut();
let ref_imut = cell.borrow(); // PANIC! Já existe um borrow mutável ativo
// thread 'main' panicked at 'already borrowed: BorrowMutError'
Boas práticas para evitar panics:
- Mantenha escopos pequenos para borrows
- Use try_borrow() quando não tiver certeza
- Documente quando borrows podem conflitar
- Considere usar Cell<T> para tipos Copy
6. Alternativas e Casos de Uso
Cell
- Cell<T>: para tipos Copy, sem referências, mais eficiente
- RefCell<T>: para tipos não-Copy, permite referências
use std::cell::Cell;
let cell = Cell::new(42);
cell.set(100); // Não precisa de borrow mutável
println!("{}", cell.get()); // 100
Para concorrência: use Mutex<T> ou RwLock<T> em vez de RefCell<T>:
use std::sync::Mutex;
let mutex = Mutex::new(0);
let mut data = mutex.lock().unwrap();
*data += 1;
Em testes e mocks: RefCell é ideal para criar mocks que registram chamadas:
struct MockDatabase {
chamadas: RefCell<Vec<String>>,
}
impl MockDatabase {
fn executar(&self, query: &str) {
self.chamadas.borrow_mut().push(query.to_string());
}
fn quantas_chamadas(&self) -> usize {
self.chamadas.borrow().len()
}
}
7. Padrões Avançados com RefCell
Cache mutável com RefCell:
struct Cache<T> {
dados: RefCell<HashMap<String, T>>,
}
impl<T: Clone> Cache<T> {
fn get_or_insert(&self, chave: &str, f: impl FnOnce() -> T) -> T {
if let Some(valor) = self.dados.borrow().get(chave) {
return valor.clone();
}
let novo_valor = f();
self.dados.borrow_mut().insert(chave.to_string(), novo_valor.clone());
novo_valor
}
}
RefCell com closures mutáveis:
let contador = Rc::new(RefCell::new(0));
let closure = {
let contador = Rc::clone(&contador);
move || {
*contador.borrow_mut() += 1;
*contador.borrow()
}
};
println!("{}", closure()); // 1
println!("{}", closure()); // 2
Estruturas auto-referenciadas: Cuidado! RefCell não resolve todos os problemas de auto-referência. Use unsafe ou bibliotecas como ouroboros para casos complexos.
Conclusão
RefCell<T> é uma ferramenta poderosa que oferece mutabilidade interior em Rust, movendo a verificação de borrowing do tempo de compilação para o tempo de execução. Use com sabedoria: ele resolve problemas reais de design, mas introduz a possibilidade de panics em runtime. Combine com Rc<T> para criar estruturas de dados compartilhadas e mutáveis, mas sempre considere alternativas mais seguras como Cell<T> para tipos simples ou Mutex<T> para concorrência.
Referências
- The Rust Reference - Interior Mutability — Documentação oficial explicando o conceito de interior mutability no sistema de tipos do Rust
- std::cell::RefCell - Rust Documentation — Documentação completa da API do RefCell com exemplos detalhados
- Rust Book - RefCell
and the Interior Mutability Pattern — Capítulo oficial do Rust Book sobre o padrão interior mutability - Rust by Example - RefCell — Exemplos práticos de uso do RefCell combinado com Rc
- Rustnomicon - Interior Mutability — Discussão avançada sobre interior mutability no Rustonomicon, incluindo considerações de segurança
- Learning Rust With Entirely Too Many Linked Lists - RefCell — Tutorial prático implementando listas ligadas com RefCell e Rc