`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>>`.
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.
- Explicar qué aporta cada capa:
Rcla propiedad compartida,RefCellla mutación interior. - Construir un árbol de nodos con hijos
Rc<RefCell<Nodo>>y padreWeak. - Reconocer el riesgo de fuga por ciclos de
Rcy romperlo conWeak. - 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.
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
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.
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.
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).
- Crea un
Rc<RefCell<Vec<i32>>>, clónalo, muta el vector desde el clon conborrow_mut().push(...)y comprueba que el original ve el cambio. ImprimeRc::strong_count. - Implementa el
struct Nodoconhijos: RefCell<Vec<Rc<Nodo>>>ypadre: RefCell<Weak<Nodo>>. Enlaza una hoja a su raíz condowngradey recupera el padre conupgrade. - Explica, con recuentos concretos, por qué usar
Rcen vez deWeakpara el enlace al padre haría que la memoria nunca se liberase. - Intenta enviar un
Rc<RefCell<i32>>a unthread::spawny lee el error del compilador; nombra qué trait falta y por qué. - Reescribe un contador compartido con
Arc<Mutex<i32>>incrementado desde cuatro hilos, y explica qué hacelockqueborrow_mutno haría.