wandres.dev
INTERIOR MUTABILITY · Cell, RefCell

`Rc<RefCell<T>>` y `Arc<Mutex<T>>`: estado compartido y mutable

`Rc<T>` da varios dueños pero solo acceso compartido; `RefCell<T>` da mutación tras un `&` pero solo un dueño. Apilados, `Rc<RefCell<T>>` compone justo lo que ninguno logra solo: varios dueños que además pueden mutar. Es el patrón canónico del estado compartido y mutable en un hilo —grafos, árboles con punteros al padre—, con `Weak` para romper ciclos. Su equivalente multi-hilo es `Arc<Mutex<T>>`.

⏱ 20 min

Dos herramientas, cada una con media solución. Rc<T> te da propiedad compartida —varios dueños del mismo dato, contados por referencia—, pero solo te presta acceso &: no puedes mutar a través de él. RefCell<T> te da mutación tras un & compartido, pero es de un solo dueño: no lo puedes duplicar. Ninguno, por separado, te deja lo que a menudo necesitas de verdad: varios dueños que además puedan modificar el dato común. La solución no es un tipo nuevo, sino la composición de los dos: Rc<RefCell<T>>. El Rc reparte la propiedad; el RefCell, encajado dentro, concede la mutación. Es el patrón con el que se construyen grafos, árboles con enlaces al padre y todo estado compartido y mutable de un solo hilo.

🎯 Al terminar esta lección sabrás
  • Explicar qué aporta cada capa: Rc la propiedad compartida, RefCell la mutación interior.
  • Construir un árbol de nodos con hijos Rc<RefCell<Nodo>> y padre Weak.
  • Reconocer el riesgo de fuga por ciclos de Rc y romperlo con Weak.
  • Trasladar el patrón a varios hilos con Arc<Mutex<T>>.

Apilar las dos mitades

La composición se lee de fuera adentro: Rc es la capa externa —la que se clona para repartir dueños—, y RefCell la interna —la que cada dueño usa para mutar—. Clonar el Rc no copia el dato: copia el puntero y sube el recuento, de modo que todos comparten el mismo RefCell.

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

let compartido = Rc::new(RefCell::new(vec![1, 2, 3]));

let otro = Rc::clone(&compartido);      // segundo dueno del MISMO RefCell
otro.borrow_mut().push(4);              // muta a traves de una de las manos

assert_eq!(*compartido.borrow(), vec![1, 2, 3, 4]);   // la otra mano lo ve
assert_eq!(Rc::strong_count(&compartido), 2);

Ambos bindings apuntan al mismo RefCell; el push hecho por otro lo observa compartido. Cada capa hace su trabajo: Rc garantiza que el dato vive mientras quede un dueño; RefCell garantiza, en ejecución, que las mutaciones no se solapan.

El árbol con enlace al padre

El caso donde este patrón se vuelve imprescindible es una estructura con referencias en dos sentidos: un árbol cuyos nodos conocen a sus hijos y a su padre. Los hijos se poseen con Rc (el padre los mantiene vivos); el padre se referencia con Weak (un puntero que no cuenta para la propiedad):

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Nodo {
    valor: i32,
    padre: RefCell<Weak<Nodo>>,        // hacia arriba: no posee
    hijos: RefCell<Vec<Rc<Nodo>>>,     // hacia abajo: posee
}

fn main() {
    let raiz = Rc::new(Nodo {
        valor: 1,
        padre: RefCell::new(Weak::new()),
        hijos: RefCell::new(vec![]),
    });

    let hoja = Rc::new(Nodo {
        valor: 2,
        padre: RefCell::new(Weak::new()),
        hijos: RefCell::new(vec![]),
    });

    *hoja.padre.borrow_mut() = Rc::downgrade(&raiz);   // enlace debil al padre
    raiz.hijos.borrow_mut().push(Rc::clone(&hoja));    // enlace fuerte al hijo

    let padre = hoja.padre.borrow().upgrade();          // Weak -> Option<Rc>
    assert_eq!(padre.unwrap().valor, 1);
}

Rc::downgrade produce un Weak, que para leerse debe upgrade a un Option<Rc>Some si el destino sigue vivo, None si ya murió—. El RefCell alrededor de cada campo es lo que permite reconfigurar el árbol —añadir hijos, cambiar el padre— a través de los & compartidos que reparte el Rc.

⚠️
Dos Rc que se apuntan filtran memoria

Si el hijo poseyera al padre con Rc y el padre al hijo también con Rc, tendrías un ciclo de referencias fuertes: cada uno mantiene vivo al otro, sus recuentos nunca llegan a cero y la memoria no se libera jamás —una fuga, aun en Rust—. Por eso el enlace “hacia atrás” (hijo a padre, nodo a nodo en una lista doble) usa Weak: referencia sin poseer, no suma al recuento fuerte y rompe el ciclo. Regla práctica: la propiedad fluye en un sentido con Rc; los enlaces de vuelta van con Weak.

El salto a varios hilos: Arc<Mutex<T>>

Rc<RefCell<T>> es, a propósito, de un solo hilo: ambos son !Send y !Sync, y el compilador te impedirá enviarlos a otro hilo. Es una protección, no una carencia: sus contadores y banderas no son atómicos. Para el mismo patrón entre hilos, se cambia cada capa por su versión sincronizada:

🔗

Rc → Arc

Arc<T> es el Rc con recuento atómico: varios hilos pueden poseer y clonar el mismo dato con seguridad. Paga el coste de las operaciones atómicas.

🔒

RefCell → Mutex

Mutex<T> da mutación interior serializando el acceso con un cerrojo: lock bloquea hasta que sea tu turno. Donde RefCell haría panic, Mutex hace esperar.

use std::sync::{Arc, Mutex};
use std::thread;

let contador = Arc::new(Mutex::new(0));
let mut manos = vec![];

for _ in 0..4 {
    let c = Arc::clone(&contador);
    manos.push(thread::spawn(move || {
        let mut n = c.lock().unwrap();   // adquiere el cerrojo; se suelta al salir
        *n += 1;
    }));
}
for h in manos { h.join().unwrap(); }
assert_eq!(*contador.lock().unwrap(), 4);
flowchart TB
subgraph Un hilo
  rc[Rc aporta varios duenos] --> combo[Rc de RefCell de T]
  refcell[RefCell aporta mutacion tras un ampersand] --> combo
  combo --> uso[N duenos que pueden mutar]
end
subgraph Varios hilos
  arc[Arc recuento atomico] --> combo2[Arc de Mutex de T]
  mutex[Mutex cerrojo con exclusion] --> combo2
  combo2 --> uso2[Estado compartido entre hilos]
end
style combo fill:#89b4fa,color:#11111b
style combo2 fill:#cba6f7,color:#11111b
style uso fill:#a6e3a1,color:#11111b
style uso2 fill:#a6e3a1,color:#11111b
💡
RwLock cuando las lecturas dominan

Mutex concede acceso exclusivo siempre, incluso a quien solo quiere leer. Si tu estado compartido se lee mucho más de lo que se escribe, RwLock<T> distingue los dos casos: permite muchos lectores a la vez con read o un único escritor con write —la misma regla de RefCell, pero con bloqueo entre hilos en lugar de panic—. Es a Mutex lo que un cerrojo matizado sería a uno tosco: más concurrencia cuando el patrón de acceso lo justifica.

Rust no prohíbe el estado compartido y mutable: te obliga a nombrarlo y a pagarlo exacto

Un lenguaje con recolector de basura te regala el estado compartido y mutable por defecto: cualquier objeto es un puntero, cualquier puntero se copia, y todos mutan el mismo objeto sin que nada te lo recuerde ni te cobre visiblemente. Esa comodidad tiene un reverso que solo se manifiesta tarde: no sabes quién puede mutar qué, ni cuándo se libera nada, ni qué mutaciones concurrentes se pisan. Rust hace la elección opuesta y la hace explícita hasta el detalle. El estado compartido y mutable no está prohibido —Rc<RefCell<T>> existe, es idiomático, y para grafos y árboles es la respuesta correcta—, pero no viene gratis ni oculto: cada capacidad que en otro lenguaje se da por supuesta, aquí la escribes como una capa del tipo y pagas su coste concreto y localizado. ¿Quieres varios dueños? Rc, y con él un recuento que sube y baja. ¿Quieres mutar el dato común? RefCell, y con él una bandera de préstamos y el riesgo de panic. ¿Lo quieres entre hilos? Arc y Mutex, y con ellos operaciones atómicas y bloqueos que pueden esperar o interbloquear. El tipo Arc<Mutex<Vec<Tarea>>> no es verboso por capricho: es una factura desglosada. Leyéndolo sabes exactamente qué estás comprando —propiedad atómica compartida, exclusión mutua, un vector dentro— y, por omisión, qué no —no dice RwLock, luego no hay lecturas concurrentes; no dice Weak, luego cuidado con los ciclos—. Esta es la filosofía que recorre todo Rust: no negarte el poder, sino negarte la ignorancia sobre lo que cuesta ejercerlo. Los ciclos que filtran, los panic por doble préstamo, los interbloqueos: no desaparecen porque estés en Rust, pero dejan de ser sorpresas invisibles y se vuelven consecuencias legibles de las capas que tú apilaste. Componer punteros inteligentes es, en el fondo, redactar con precisión el contrato de tu estado compartido, y leer un tipo compuesto es leer ese contrato. Ahí está la madurez: no en evitar Rc<RefCell<T>>, sino en saber, al escribirlo, cada palabra de lo que promete y de lo que te cobra.

📝
Lo esencial del patrón compartido y mutable

Rc<RefCell<T>> compone lo que ninguna capa logra sola: Rc reparte varios dueños del mismo dato, RefCell deja que cada uno lo mute tras un &. Es el patrón de grafos y árboles; los enlaces de vuelta (al padre) usan Weak para no formar ciclos de Rc que filtrarían memoria, y se leen con upgrade. Todo esto es de un solo hilo (!Send). Para el mismo patrón entre hilos se cambia cada capa por su versión sincronizada: Arc (recuento atómico) y Mutex (exclusión por cerrojo, que espera en vez de hacer panic).

⚔️ Comparte y muta con varias manos
  1. Crea un Rc<RefCell<Vec<i32>>>, clónalo, muta el vector desde el clon con borrow_mut().push(...) y comprueba que el original ve el cambio. Imprime Rc::strong_count.
  2. Implementa el struct Nodo con hijos: RefCell<Vec<Rc<Nodo>>> y padre: RefCell<Weak<Nodo>>. Enlaza una hoja a su raíz con downgrade y recupera el padre con upgrade.
  3. Explica, con recuentos concretos, por qué usar Rc en vez de Weak para el enlace al padre haría que la memoria nunca se liberase.
  4. Intenta enviar un Rc<RefCell<i32>> a un thread::spawn y lee el error del compilador; nombra qué trait falta y por qué.
  5. Reescribe un contador compartido con Arc<Mutex<i32>> incrementado desde cuatro hilos, y explica qué hace lock que borrow_mut no haría.