RefCell e mutabilidade interior
Esta aula aborda o padrão de mutabilidade interior em Rust, focando no tipo RefCell<T>. Explica como borrow() e borrow_mut() permitem violar as regras de empréstimo em tempo de compilação, delegando a verificação para o runtime, e como combiná-lo com Rc<T> para criar dados mutáveis compartilhados. Também discute os riscos de pânico e vazamento de referências.
A mutabilidade interior é um padrão de design em Rust que permite modificar dados mesmo quando existem referências imutáveis para eles. Isso é possível graças ao tipo RefCell<T>, que realiza a verificação das regras de empréstimo em tempo de execução, em vez de em tempo de compilação. Essa flexibilidade é útil em cenários como callbacks, caches ou quando a estrutura de dados exige mutabilidade interior, mas deve ser usada com cuidado para evitar pânicos ou violações de segurança.
Nesta aula, vamos explorar os métodos borrow() e borrow_mut(), entender como a checagem em runtime funciona, combinar RefCell com Rc para criar dados mutáveis compartilhados e discutir os riscos associados a esse padrão.
borrow e borrow_mut
Os métodos borrow() e borrow_mut() são as principais formas de acessar o valor interno de um RefCell. borrow() retorna uma referência imutável (&T), enquanto borrow_mut() retorna uma referência mutável (&mut T). Ambos seguem as mesmas regras de empréstimo do Rust: você pode ter várias referências imutáveis simultâneas ou uma única referência mutável, mas não ambas ao mesmo tempo.
A diferença é que, com RefCell, essas regras são verificadas em tempo de execução. Se você tentar violá-las (por exemplo, chamar borrow_mut() enquanto já existe um borrow() ativo), o programa entra em pânico (panic). Isso significa que o código compila, mas pode falhar em execução. Exemplo:
use std::cell::RefCell;
let data = RefCell::new(42);
let ref1 = data.borrow();
let ref2 = data.borrow(); // OK: duas referências imutáveis
println!("{} {}", ref1, ref2);
// let ref_mut = data.borrow_mut(); // Isso causaria pânico, pois ref1 e ref2 ainda estão ativas
O método borrow() retorna um Ref<T>, que implementa Deref para &T. Já borrow_mut() retorna um RefMut<T>, que implementa Deref para &mut T. Ambos os tipos liberam o empréstimo quando saem de escopo, permitindo novas operações.
Checagem em runtime
A principal característica do RefCell é que a verificação das regras de empréstimo ocorre em tempo de execução, não em tempo de compilação. Isso é feito através de um contador de referências interno: o RefCell mantém um contador de quantas referências imutáveis estão ativas e se há uma referência mutável ativa.
Quando você chama borrow(), o contador de imutáveis é incrementado. Se já houver uma referência mutável ativa, o programa entra em pânico. Quando o Ref é dropado, o contador é decrementado. O mesmo vale para borrow_mut(), que verifica se não há nenhuma referência (imutável ou mutável) ativa. Essa abordagem permite contornar as restrições do borrow checker, mas com o custo de uma pequena overhead em tempo de execução e o risco de pânico.
Exemplo de pânico em runtime:
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(10);
let _ref_mut = cell.borrow_mut();
let _ref_immut = cell.borrow(); // Isso causa pânico!
}
Ao executar, o programa exibe uma mensagem como "already borrowed: BorrowMutError" e aborta. Isso mostra que a segurança ainda existe, mas é diferida para o runtime.
Rc<RefCell<T>>
Uma combinação poderosa é usar Rc<RefCell<T>> para criar dados mutáveis compartilhados com múltiplos donos. Rc permite que vários ponteiros compartilhem a posse do valor, mas por padrão ele só fornece acesso imutável. Ao envolver o valor em RefCell, podemos obter mutabilidade interior, permitindo modificar o valor através de qualquer referência Rc.
Isso é extremamente útil em situações como implementar um grafo onde vários nós podem referenciar um mesmo nó mutável, ou em padrões de UI onde múltiplos componentes precisam alterar um estado compartilhado.
Exemplo:
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(5));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
*clone1.borrow_mut() += 1;
*clone2.borrow_mut() += 2;
println!("{:?}", shared.borrow()); // 8
Note que borrow_mut() retorna um RefMut, que é desreferenciado com * para acessar o valor interno. Esse padrão é comum em Rust para compartilhar estado mutável, mas lembre-se de que a verificação ainda é em runtime.
Riscos
O uso de RefCell e Rc<RefCell<T>> traz riscos que devem ser considerados. O principal é o pânico em runtime se as regras de empréstimo forem violadas. Isso pode tornar o código menos previsível, especialmente em sistemas críticos onde pânicos não são aceitáveis. Além disso, o overhead de contagem de referências e verificação pode impactar desempenho em loops internos.
Outro risco é a possibilidade de criar referências penduradas (dangling references) se você armazenar referências do RefCell em locais que sobrevivem ao Ref ou RefMut. Por exemplo, se você guardar o Ref retornado por borrow() em uma estrutura que vive mais que o RefCell, o programa pode acessar memória inválida. No entanto, o Rust impede isso em tempo de compilação, pois Ref e RefMut têm lifetimes ligados ao RefCell.
Por fim, o uso excessivo de mutabilidade interior pode tornar o código difícil de entender e depurar, pois as mutações podem ocorrer em qualquer lugar onde haja uma referência compartilhada. É recomendável usar esse padrão apenas quando necessário e documentar claramente o comportamento.
Boas práticas
Prefira usar Cell<T> quando o valor for do tipo Copy, pois ele não requer borrow checking em runtime. Use RefCell apenas quando precisar de mutabilidade interior com tipos não-Copy. Evite manter Ref ou RefMut por longos períodos; libere-os o mais rápido possível para reduzir a chance de pânico. Considere usar Mutex ou RwLock em cenários concorrentes com threads, pois RefCell não é thread-safe (não implementa Sync).
Referências
- Documentação oficial do RefCell
- The Rust Book: Interior Mutability
- Rust by Example: Rc
- std::cell module documentation
- The Rustonomicon: Borrow Splitting
Exercícios
Crie um
RefCellcontendo um inteiro 0. Em seguida, obtenha duas referências imutáveis e imprima seus valores. Depois, obtenha uma referência mutável e incremente o valor para 10. Verifique o valor final.✓ Resposta:use std::cell::RefCell; fn main() { let cell = RefCell::new(0); let r1 = cell.borrow(); let r2 = cell.borrow(); println!("r1: {}, r2: {}", r1, r2); drop(r1); drop(r2); let mut r3 = cell.borrow_mut(); *r3 = 10; drop(r3); println!("Final: {}", cell.borrow()); }Escreva um programa que cause um pânico ao tentar obter uma referência mutável enquanto uma referência imutável está ativa. Explique por que o pânico ocorre.
✓ Resposta:use std::cell::RefCell; fn main() { let cell = RefCell::new(42); let _r = cell.borrow(); // referência imutável ativa let _m = cell.borrow_mut(); // pânico: já emprestado imutavelmente }O pânico ocorre porque o
RefCelldetecta que já existe uma referência imutável ativa e não permite uma referência mutável, violando as regras de empréstimo.Implemente um exemplo usando
Rc<RefCell<Vec<i32>>>onde duas funções diferentes adicionam elementos ao vetor compartilhado. Mostre o resultado final.✓ Resposta:use std::rc::Rc; use std::cell::RefCell; fn add_one(data: Rc<RefCell<Vec<i32>>>) { data.borrow_mut().push(1); } fn add_two(data: Rc<RefCell<Vec<i32>>>) { data.borrow_mut().push(2); } fn main() { let shared = Rc::new(RefCell::new(vec![])); add_one(Rc::clone(&shared)); add_two(Rc::clone(&shared)); println!("{:?}", shared.borrow()); // [1, 2] }Explique por que
RefCellnão éSynce como isso afeta o uso em múltiplas threads. Dê um exemplo que não compila.✓ Resposta:RefCellnão implementaSyncporque sua verificação de empréstimo não é atômica, o que poderia causar corridas de dados se usado em múltiplas threads. O seguinte código não compila:use std::sync::Arc; use std::cell::RefCell; fn main() { let cell = Arc::new(RefCell::new(0)); std::thread::spawn(move || { *cell.borrow_mut() = 1; }); }O compilador reclama que
RefCell<i32>não pode ser compartilhado entre threads porque não implementaSync.Crie uma estrutura
Personcom um camponame: RefCell<String>e um método que altera o nome. UseRcpara compartilhar a pessoa entre duas funções que modificam o nome. Mostre o nome final.✓ Resposta:use std::rc::Rc; use std::cell::RefCell; struct Person { name: RefCell<String>, } impl Person { fn new(name: &str) -> Person { Person { name: RefCell::new(name.to_string()) } } fn rename(&self, new_name: &str) { *self.name.borrow_mut() = new_name.to_string(); } } fn main() { let person = Rc::new(Person::new("Alice")); let p1 = Rc::clone(&person); let p2 = Rc::clone(&person); p1.rename("Bob"); p2.rename("Charlie"); println!("Final name: {}", person.name.borrow()); // Charlie }