El borrow checker de Rust comprueba en tiempo de compilación que las referencias sean válidas. Esto es muy potente, pero a veces es demasiado conservador: puede rechazar código perfectamente correcto en el que la lógica garantiza que no habrá conflictos. Cell<T> y RefCell<T> aplican esas mismas reglas en runtime, permitiendo modificar datos a través de referencias inmutables cuando el compilador no puede demostrarlo por sí solo.
Cell<T>: mutabilidad sin referencias
Cell funciona con tipos que implementan Copy. No devuelve referencias: copia el valor hacia dentro y hacia fuera. Sin overhead de comprobaciones en runtime, y sin posibilidad de pánico.
use std::cell::Cell;
fn main() {
let x = Cell::new(5);
// x es inmutable desde fuera, pero podemos modificar el interior
x.set(10);
println!("{}", x.get()); // 10
// Útil en structs que necesitan estado interno sin &mut self
struct Contador {
valor: Cell<u32>,
}
impl Contador {
fn new() -> Self { Self { valor: Cell::new(0) } }
fn incrementar(&self) { self.valor.set(self.valor.get() + 1); }
fn valor(&self) -> u32 { self.valor.get() }
}
let c = Contador::new();
c.incrementar();
c.incrementar();
println!("Contador: {}", c.valor()); // 2
}
RefCell<T>: préstamos comprobados en runtime
RefCell mantiene un contador interno de préstamos. Puedes pedir prestado el interior con borrow() (inmutable) o con borrow_mut() (mutable). Si en runtime se violan las reglas del borrow checker, el programa entra en pánico.
use std::cell::RefCell;
fn main() {
let datos = RefCell::new(vec![1, 2, 3]);
// Préstamo inmutable
{
let r = datos.borrow();
println!("Longitud: {}", r.len()); // 3
} // r sale de scope ? préstamo liberado
// Préstamo mutable
{
let mut r = datos.borrow_mut();
r.push(4);
}
println!("{:?}", datos.borrow()); // [1, 2, 3, 4]
}
Caso práctico: cache perezosa
use std::cell::RefCell;
struct ExpensiveCache {
cache: RefCell<Option<String>>,
}
impl ExpensiveCache {
fn new() -> Self {
Self { cache: RefCell::new(None) }
}
fn get_value(&self) -> String {
if self.cache.borrow().is_none() {
// Calcular el valor costoso
let resultado = "resultado muy caro".to_string();
*self.cache.borrow_mut() = Some(resultado);
}
self.cache.borrow().clone().unwrap()
}
}
fn main() {
let cache = ExpensiveCache::new();
println!("{}", cache.get_value()); // calcula
println!("{}", cache.get_value()); // usa caché
}
El pánico que verás si abusas de RefCell
use std::cell::RefCell;
fn main() {
let datos = RefCell::new(vec![1, 2, 3]);
let r1 = datos.borrow();
let r2 = datos.borrow_mut(); // PÁNICO en runtime
// thread 'main' panicked at 'already borrowed:
// BorrowMutError'
println!("{:?}", r1);
}
Rc<RefCell<T>>: múltiples propietarios mutables
La combinación más habitual de estos tipos: varios propietarios que necesitan mutar el dato compartido.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let lista = Rc::new(RefCell::new(vec![1, 2, 3]));
let a = Rc::clone(&lista);
let b = Rc::clone(&lista);
// Ambos pueden mutar el interior
a.borrow_mut().push(4);
b.borrow_mut().push(5);
println!("{:?}", lista.borrow()); // [1, 2, 3, 4, 5]
}
// Para múltiples hilos usa Arc<Mutex<T>> en su lugar
try_borrow y try_borrow_mut
use std::cell::RefCell;
fn main() {
let datos = RefCell::new(42);
let r1 = datos.borrow();
// try_borrow_mut devuelve Result en lugar de entrar en pánico
match datos.try_borrow_mut() {
Ok(mut v) => *v += 1,
Err(e) => println!("No se pudo prestar: {e}"),
}
// "No se pudo prestar: already borrowed"
drop(r1); // liberar el préstamo
if let Ok(mut v) = datos.try_borrow_mut() {
*v += 1;
}
println!("{}", datos.borrow()); // 43
}
Resumen
Cell<T>copia valores dentro y fuera sin referencias; ideal para tiposCopycon mutabilidad interior sin riesgo de pánico.RefCell<T>aplaza las comprobaciones del borrow checker al runtime; puede entrar en pánico si se violan las reglas.borrow()yborrow_mut()devuelven guardas que se liberan al salir de scope.try_borrow_mut()devuelveResultpara evitar pánicos cuando hay incertidumbre.Rc<RefCell<T>>es el patrón estándar para múltiples propietarios mutables en un solo hilo.
