RefCell y Cell en Rust: mutabilidad interior y el borrow checker en runtime

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 tipos Copy con 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() y borrow_mut() devuelven guardas que se liberan al salir de scope.
  • try_borrow_mut() devuelve Result para evitar pánicos cuando hay incertidumbre.
  • Rc<RefCell<T>> es el patrón estándar para múltiples propietarios mutables en un solo hilo.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP