Rc de T: propiedad compartida por conteo de referencias
`Box` tiene un único dueño; pero a veces un mismo dato necesita varios. `Rc<T>` (reference counted) reparte la propiedad de un valor entre muchos dueños: `clone` no copia el dato, incrementa un contador; el valor muere cuando cae el último dueño. Su representación con contadores junto al dato, por qué solo da acceso compartido y por qué no es apto para hilos.
Box impone un dueño único, y casi siempre es lo correcto. Pero algunas estructuras piden que un mismo dato tenga varios dueños: un nodo de un grafo al que apuntan varios padres, una configuración que media docena de componentes comparten, la cola compartida de dos listas. Ahí la propiedad exclusiva estorba. Rc<T> —reference counted, contado por referencias— reparte la propiedad: cada dueño posee una participación, clone no duplica el dato sino que suma uno a un contador, y el valor vive hasta que la última participación se libera. Es propiedad compartida deterministas, sin recolector de basura y sin coste de fondo. El precio: un solo hilo.
- Compartir la propiedad de un mismo valor con
Rc::newyRc::clone. - Entender el conteo fuerte:
cloneincrementa,dropdecrementa, y en cero se libera. - Ver la representación en memoria: los contadores viven junto al dato en una sola asignación.
- Reconocer por qué
Rcsolo da acceso compartido y por qué no implementaSendniSync.
Varios dueños para un mismo dato
Con Box no puedes construir dos listas que compartan una misma cola: mover la cola a la primera la deja indisponible para la segunda, y el borrow checker lo rechaza. Rc disuelve el problema dando a cada uno su propia participación sobre el mismo dato:
use std::rc::Rc;
enum Lista {
Cons(i32, Rc<Lista>),
Nil,
}
use Lista::{Cons, Nil};
fn main() {
let compartida = Rc::new(Cons(10, Rc::new(Nil)));
let a = Cons(3, Rc::clone(&compartida)); // a apunta a la cola compartida
let b = Cons(4, Rc::clone(&compartida)); // b tambien, sin copiarla
}
Rc::clone(&compartida) no clona en profundidad el Cons(10, ...): fabrica otro Rc que apunta al mismo sitio y suma uno al contador. Es una operación de coste constante, no proporcional al tamaño del dato. La convención de escribir Rc::clone(&x) en vez de x.clone() es deliberada: deja a la vista que es un incremento barato de contador y no una copia costosa.
El conteo de referencias
La mecánica es un contador que sube con cada clone y baja con cada drop; al llegar a cero, el valor y su asignación se liberan. Rc::strong_count te deja observarlo:
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("dato"));
println!("cuenta = {}", Rc::strong_count(&a)); // 1
let _b = Rc::clone(&a);
println!("cuenta = {}", Rc::strong_count(&a)); // 2
{
let _c = Rc::clone(&a);
println!("cuenta = {}", Rc::strong_count(&a)); // 3
} // _c sale de scope: su Drop baja la cuenta a 2
println!("cuenta = {}", Rc::strong_count(&a)); // 2
} // aqui caen _b y a: la cuenta llega a 0 y la String se libera
No hay que llamar a nada manualmente: el Drop de cada Rc decrementa por ti, y el último en caer ejecuta la destrucción del valor. Es la misma liberación determinista de Box, solo que compartida entre varios puntos del programa que dejan de existir en momentos distintos.
La representación en memoria
Un Rc<T> apunta a una asignación interna —el RcBox— que guarda, contiguos, el contador fuerte, el contador débil (lo verás con Weak) y el valor T. Los contadores viven pegados al dato, en una única asignación; el Rc<T> en sí es una sola palabra: un puntero a esa caja. Por eso todos los clones comparten el mismo contador —apuntan a la misma caja— y por eso clonar es tan barato.
flowchart TB a[Rc a en pila] --> caja b[Rc b en pila] --> caja c[Rc c en pila] --> caja subgraph caja[RcBox en el heap] sc[contador fuerte 3] wc[contador debil 0] val[valor T] end style a fill:#89b4fa,color:#11111b style b fill:#89b4fa,color:#11111b style c fill:#89b4fa,color:#11111b style val fill:#a6e3a1,color:#11111b
Compartido no es mutable
Rc<T> implementa Deref hacia T pero no DerefMut: solo obtienes &T, nunca &mut T. Es forzoso: si varios dueños comparten el dato y cualquiera pudiera pedir &mut, tendrías aliasing mutable —dos referencias mutables vivas al mismo valor— que es exactamente lo que el borrow checker prohíbe. Para mutar un dato compartido necesitas mutabilidad interior: envolver el contenido en un RefCell, dando Rc<RefCell<T>>, que traslada la comprobación de préstamos a tiempo de ejecución. Lo montarás en la última lección del nivel.
Hay una grieta segura: Rc::get_mut devuelve Some(&mut T) solo si la cuenta fuerte es 1 y no hay débiles, es decir, si de hecho no compartes con nadie en ese instante. Si hay más dueños, devuelve None, porque conceder mutabilidad rompería a los demás.
Por qué Rc no cruza hilos
El contador de Rc es un entero normal, incrementado y decrementado sin sincronización. Es rapidísimo, pero frágil ante la concurrencia: si dos hilos clonaran o soltaran el mismo Rc a la vez, las dos operaciones sobre el contador podrían entrelazarse y perder una cuenta, provocando una doble liberación o una fuga. Rust lo impide de raíz: Rc<T> es !Send y !Sync, así que el compilador rechaza moverlo o compartirlo entre hilos. No es una recomendación, es un error de compilación. Para compartir entre hilos existe Arc, la siguiente lección, que paga contadores atómicos a cambio de esa seguridad.
Ambos hacen lo mismo, pero escribe Rc::clone(&x). La forma explícita comunica al lector que es un incremento de contador de coste constante, no una copia profunda del dato. Cuando más tarde busques cuellos de botella, los Rc::clone son ruido inocente; los .clone() de verdad —los que duplican Vec o String— son los sospechosos. Mezclarlos bajo el mismo .clone() te esconde esa distinción.
Rc parece un truco pequeño —un contador— pero es una filosofía entera sobre cuándo muere un dato. Un recolector de basura por rastreo responde a esa pregunta de forma global y diferida: cada cierto tiempo detiene el mundo, recorre el grafo de objetos alcanzables y libera el resto, en un momento que tú no eliges y con una pausa que tú no controlas. El conteo de referencias responde de forma local e inmediata: cada dueño sabe únicamente que sostiene una participación, y la última en soltarse —la que lleva el contador a cero— destruye el valor ahí mismo, en la instrucción exacta en que ese Rc cae. Nadie tiene la vista global; la corrección emerge de una regla puramente local repetida en cada clone y cada drop. Esto compra dos cosas que un GC no da: determinismo —sabes que el destructor corre justo cuando muere el último dueño, no cuando al recolector le apetezca— y coste distribuido —no hay hilo de fondo ni pausas, solo una suma o una resta por operación—. Y compra la elección de no pagar por lo que no usas: como el contador de Rc es un entero corriente, no un atómico, compartir dentro de un hilo cuesta lo mínimo posible; el precio de la sincronización se paga solo cuando de verdad cruzas hilos, y entonces cambias a Arc. Que esa frontera esté vigilada por el sistema de tipos —Rc es !Send, punto— significa que el compromiso “rápido pero de un hilo” no es un pacto de caballeros que puedas romper por descuido, sino un teorema que el compilador te demuestra. Rc no es solo memoria compartida: es la afirmación de que la gestión de memoria puede ser precisa, local y gratuita cuando el problema es local.
Rc<T> reparte la propiedad de un valor por conteo de referencias, en un solo hilo. Rc::clone incrementa el contador —coste constante, no copia el dato— y cada drop lo decrementa; al llegar a cero, el valor se libera de forma determinista. Los dos contadores viven en el RcBox, junto al dato, en una única asignación; el Rc en sí es un puntero de una palabra. Solo entrega &T: para mutar lo compartido necesitas Rc<RefCell<T>>. Es !Send y !Sync por su contador no atómico; para cruzar hilos existe Arc.
- Construye tres listas
a,bycque compartan una misma cola conRc, e imprimeRc::strong_countde la cola después de cadaclone. - Envuelve un
Stringen unRc, clónalo dentro de un bloque anidado y observa cómo la cuenta sube y baja al entrar y salir del scope. Explica quién ejecuta cada decremento. - Intenta obtener
&mutdel valor con*rc = ...y lee el error. Después usaRc::get_mutcon cuenta 1 y con cuenta 2, y explica por qué solo funciona en el primer caso. - Intenta mover un
Rca unthread::spawny lee el mensaje sobreSend. Relaciónalo con la naturaleza no atómica del contador. - Dibuja el
RcBoxen memoria para una cuenta de 3: sitúa los dos contadores y el valor, y explica por qué todos los clones ven el mismo contador.