RefCell en Rust: mutabilidad interior cuando el borrow checker es demasiado estricto

Las reglas del borrow checker de Rust se verifican en tiempo de compilación. Eso es bueno porque detecta errores antes de ejecutar el programa, pero a veces es demasiado conservador: hay patrones donde sabes que el acceso es correcto en runtime pero el compilador no puede comprobarlo estáticamente. Para esos casos existe RefCell<T>: mutabilidad interior con verificación en tiempo de ejecución.

Qué es la mutabilidad interior

Normalmente, para modificar un valor necesitas que la variable sea mut y que nadie más lo esté prestando. Con RefCell<T>, puedes modificar el valor a través de una referencia inmutable. Las reglas del borrow checker se siguen cumpliendo, pero la verificación ocurre en runtime, no en compilación.

borrow() y borrow_mut()

use std::cell::RefCell;

fn main() {
    let dato = RefCell::new(5);

    // Leer: equivalente a &dato
    let r1 = dato.borrow();
    let r2 = dato.borrow();
    println!("{} {}", r1, r2); // OK: dos préstamos inmutables
    drop(r1);
    drop(r2);

    // Modificar: equivalente a &mut dato
    let mut r3 = dato.borrow_mut();
    *r3 += 10;
    println!("{}", r3); // 15
}

Panic en runtime si se viola la regla

use std::cell::RefCell;

fn main() {
    let dato = RefCell::new(5);
    let r1 = dato.borrow();        // OK
    let r2 = dato.borrow_mut();    // PANIC en runtime: ya hay un borrow activo
    println!("{} {}", r1, r2);
}
thread 'main' panicked at 'already borrowed: BorrowMutError'

Para evitar el panic, usa try_borrow() y try_borrow_mut() que devuelven Result en lugar de paniquear.

Caso de uso: mock objects en tests

trait Mensajero {
    fn enviar(&self, mensaje: &str);
}

struct MockMensajero {
    mensajes: RefCell<Vec<String>>,
}

impl Mensajero for MockMensajero {
    fn enviar(&self, mensaje: &str) {
        // La firma requiere &self (inmutable), pero necesitamos modificar mensajes
        self.mensajes.borrow_mut().push(String::from(mensaje));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_envio() {
        let mock = MockMensajero { mensajes: RefCell::new(vec![]) };
        mock.enviar("hola");
        mock.enviar("mundo");
        assert_eq!(mock.mensajes.borrow().len(), 2);
    }
}

Caso de uso: grafo con aristas compartidas

use std::cell::RefCell;
use std::rc::Rc;

type Nodo = Rc<RefCell<NodoDato>>;

struct NodoDato {
    valor: i32,
    vecinos: Vec<Nodo>,
}

fn añadir_arista(a: &Nodo, b: &Nodo) {
    a.borrow_mut().vecinos.push(Rc::clone(b));
    b.borrow_mut().vecinos.push(Rc::clone(a));
}

fn main() {
    let n1 = Rc::new(RefCell::new(NodoDato { valor: 1, vecinos: vec![] }));
    let n2 = Rc::new(RefCell::new(NodoDato { valor: 2, vecinos: vec![] }));

    añadir_arista(&n1, &n2);

    println!("n1 tiene {} vecinos", n1.borrow().vecinos.len());
}

Cuándo usar RefCell

SituaciónHerramienta
Mutabilidad estándar (verificación en compilación)&mut T
Mutabilidad interior en un solo hiloRefCell<T>
Mutabilidad interior entre hilosMutex<T>
Solo lecturas atómicasCell<T> (tipos Copy)

Resumen

  • RefCell<T> mueve la verificación del borrow checker a runtime.
  • borrow(): referencia inmutable (&T). Puede coexistir con otras.
  • borrow_mut(): referencia mutable (&mut T). Exclusiva, panic si ya hay otra activa.
  • try_borrow() / try_borrow_mut(): versiones que devuelven Result.
  • Ideal para mock objects en tests y estructuras de datos cíclicas o grafos.
  • Solo para un único hilo. Para varios hilos, usa Mutex<T>.

El siguiente artículo cubre el patrón Rc<RefCell<T>>: múltiples propietarios con mutabilidad interior, con ejemplos de árbol con padre y grafo con aristas compartidas.

COMPARTE ESTE ARTÍCULO

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