Ir al contenido

Weak y Cow en Rust: ciclos de Rc y clone-on-write

En la página de smart pointers conociste Box, Rc y RefCell. Esta página añade dos herramientas más que resuelven dos problemas muy reales. Weak<T> te permite apuntar a un valor sin mantenerlo vivo. Cow<T> permite que una función devuelva datos prestados cuando puede, y que solo haga una copia cuando no le queda más remedio.

Piensa en Rc como la llave de una casa: mientras alguien tenga una llave, la casa sigue en pie. Weak se parece más a la dirección apuntada en un papel. Te sirve para encontrar la casa, pero si todos devuelven sus llaves y la casa desaparece, la dirección ya no lleva a ningún sitio. Nadie se queda pagando una casa de la que se había olvidado.

El problema: los ciclos de Rc nunca se limpian

Sección titulada «El problema: los ciclos de Rc nunca se limpian»

Un valor Rc se libera cuando su contador fuerte (strong count) llega a 0. Eso funciona genial hasta que dos valores guardan un Rc el uno al otro. Entonces cada uno mantiene vivo al otro, para siempre.

Para verlo, el tipo Person de abajo implementa Drop, así que imprime un mensaje cuando se limpia. Fíjate en qué mensajes aparecen:

use std::cell::RefCell;
use std::rc::Rc;
struct Person {
name: String,
friend: RefCell<Option<Rc<Person>>>,
}
impl Drop for Person {
fn drop(&mut self) {
println!("liberando a {}", self.name);
}
}
fn main() {
let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) });
let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice apunta a bob, y bob apunta de vuelta a alice
*alice.friend.borrow_mut() = Some(Rc::clone(&bob));
*bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("contador fuerte de alice: {}", Rc::strong_count(&alice));
println!("contador fuerte de bob: {}", Rc::strong_count(&bob));
println!("fin de main");
}

Salida:

contador fuerte de alice: 2
contador fuerte de bob: 2
fin de main
▶ Pruébalo tú mismo

Fíjate en lo que falta: no aparece liberando a alice ni liberando a bob. Cuando main termina, las variables alice y bob desaparecen, así que cada contador baja de 2 a 1. Pero nunca llega a 0, porque a cada persona la sigue sujetando el campo friend de la otra. Esa memoria se pierde (es una fuga de memoria, un memory leak).

No te preocupes si esto te sorprende. El compilador no puede detectarlo, y no es un fallo que cierre el programa. Es simplemente memoria que nunca vuelve. En un programa que se ejecuta durante mucho tiempo y sigue creando ciclos, eso se va acumulando.

Weak<T>: un puntero que no mantiene vivas las cosas

Sección titulada «Weak<T>: un puntero que no mantiene vivas las cosas»

La solución es hacer débil (weak) uno de los enlaces, o los dos. Obtienes un Weak a partir de un Rc con Rc::downgrade. Un enlace débil no cuenta como dueño, así que no impide que el valor se libere.

Como el valor podría haber desaparecido ya, no puedes usar un Weak directamente. Llamas a .upgrade(), que te da un Option<Rc<T>>: Some si el valor sigue vivo y None si no.

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Person {
name: String,
friend: RefCell<Weak<Person>>,
}
impl Drop for Person {
fn drop(&mut self) {
println!("liberando a {}", self.name);
}
}
fn main() {
let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) });
let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob);
*bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() {
println!("el amigo de alice es {}", friend.name);
}
println!("alice: fuertes = {}, débiles = {}", Rc::strong_count(&alice), Rc::weak_count(&alice));
println!("fin de main");
}

Salida:

el amigo de alice es bob
alice: fuertes = 1, débiles = 1
fin de main
liberando a bob
liberando a alice
▶ Pruébalo tú mismo

Esta vez aparecen las dos líneas de liberando. Alice tiene un dueño fuerte (la variable alice) y un enlace débil (el de Bob). Cuando main termina, el contador fuerte llega a 0 y Rust la limpia. Bob se va primero porque las variables se liberan en orden inverso al de su creación.

Algunas cosas que conviene recordar:

  • Weak::new() crea un puntero débil vacío que no apunta a nada. upgrade() sobre él siempre devuelve None. Es un valor inicial muy práctico.
  • Rc::strong_count cuenta los dueños. Rc::weak_count cuenta los enlaces débiles. Solo el contador fuerte decide cuándo se libera el valor.
  • El Rc que recibes de upgrade() es un dueño real y temporal. Cuando sale del ámbito (scope), el contador fuerte vuelve a bajar.

Un árbol de carpetas: los hijos se poseen, los padres son débiles

Sección titulada «Un árbol de carpetas: los hijos se poseen, los padres son débiles»

Los árboles son el sitio clásico para Weak. Una carpeta es dueña de sus subcarpetas, así que children guarda Rc. Una subcarpeta también quiere conocer a su padre, pero no debería ser su dueña. Si las dos direcciones usaran Rc, volverías a tener un ciclo. Así que parent es un Weak:

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Folder {
name: String,
parent: RefCell<Weak<Folder>>,
children: RefCell<Vec<Rc<Folder>>>,
}
fn new_folder(name: &str) -> Rc<Folder> {
Rc::new(Folder {
name: name.to_string(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
fn full_path(folder: &Folder) -> String {
match folder.parent.borrow().upgrade() {
Some(parent) => format!("{}/{}", full_path(&parent), folder.name),
None => folder.name.clone(),
}
}
fn main() {
let home = new_folder("home");
let photos = new_folder("fotos");
let trips = new_folder("viajes");
add_child(&home, Rc::clone(&photos));
add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() {
Some(parent) => println!("fotos está dentro de {}", parent.name),
None => println!("fotos no tiene padre"),
}
println!("ruta completa: {}", full_path(&trips));
println!("home: fuertes = {}, débiles = {}", Rc::strong_count(&home), Rc::weak_count(&home));
println!("fotos: fuertes = {}, débiles = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));
}

Salida:

fotos está dentro de home
ruta completa: home/fotos/viajes
home: fuertes = 1, débiles = 1
fotos: fuertes = 2, débiles = 1
▶ Pruébalo tú mismo

Lee los contadores con calma. photos tiene dos dueños fuertes: la variable photos y la lista children dentro de home. Tiene un enlace débil: el campo parent de trips. home solo tiene un dueño fuerte, su propia variable, porque nadie es dueño de su padre. Esa es justo la forma que quieres: la propiedad (ownership) fluye hacia abajo por el árbol, y los enlaces que van hacia arriba son débiles.

RefCell está ahí porque asignas parent y añades a children cuando las carpetas ya están dentro de un Rc, y Rc por sí solo solo te da acceso compartido de solo lectura.

Listas débiles que no mantienen vivas las cosas

Sección titulada «Listas débiles que no mantienen vivas las cosas»

Weak también es genial para listas de “vigila esto, pero no te aferres a ello”: listeners (oyentes), observadores o una caché de cosas que son de otro. Aquí, el marcador de un juego envía actualizaciones a quien siga escuchando. Cuando se libera un listener, su enlace débil simplemente deja de funcionar:

use std::rc::{Rc, Weak};
struct Listener {
name: String,
}
fn notify(listeners: &[Weak<Listener>], event: &str) {
for weak in listeners {
match weak.upgrade() {
Some(listener) => println!("{} recibió: {event}", listener.name),
None => println!("(un listener ya no existe, se omite)"),
}
}
}
fn main() {
let screen = Rc::new(Listener { name: String::from("pantalla") });
let logger = Rc::new(Listener { name: String::from("registro") });
let mut listeners: Vec<Weak<Listener>> = Vec::new();
listeners.push(Rc::downgrade(&screen));
listeners.push(Rc::downgrade(&logger));
notify(&listeners, "la puntuación es 10");
drop(logger); // su dueño cierra el registro
notify(&listeners, "la puntuación es 20");
// limpia los enlaces que no apuntan a nada
listeners.retain(|weak| weak.upgrade().is_some());
println!("listeners restantes: {}", listeners.len());
}

Salida:

pantalla recibió: la puntuación es 10
registro recibió: la puntuación es 10
pantalla recibió: la puntuación es 20
(un listener ya no existe, se omite)
listeners restantes: 1
▶ Pruébalo tú mismo

La lista nunca impidió que el registro se liberara. Si hubiera guardado Rc, el registro habría seguido vivo dentro de la lista, recibiendo mensajes que nadie quería. Con Weak, el dueño decide cuándo desaparece algo, y todos los demás solo lo comprueban con upgrade().

Cow: toma prestado cuando puedas, copia cuando debas

Sección titulada «Cow: toma prestado cuando puedas, copia cuando debas»

Ahora, un problema distinto. Imagina que escribes una función que limpia nombres de usuario. La mayoría de los nombres ya están bien. Unos pocos tienen espacios de más o mayúsculas. Si tu función siempre devuelve un String nuevo, haces una copia de cada nombre, incluso de los que no cambiaste.

Cow (“clone on write”, clonar al escribir, de std::borrow::Cow) resuelve esto. Un Cow<'_, str> es un enum con dos variantes:

  • Cow::Borrowed(&str): “aquí tienes el texto original, sin tocar”. No reserva memoria.
  • Cow::Owned(String): “tuve que cambiarlo, así que aquí tienes un String nuevo”.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> {
let trimmed = name.trim();
let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper {
Cow::Borrowed(name) // ya está limpio: devuelve el original
} else {
Cow::Owned(trimmed.to_lowercase()) // hay que arreglarlo: crea un String nuevo
}
}
fn main() {
for input in ["alice", " Bob ", "CAROL", "dave"] {
match normalize(input) {
Cow::Borrowed(name) => println!("{input:?} -> {name:?} (prestado, sin copia)"),
Cow::Owned(name) => println!("{input:?} -> {name:?} (propio, String nuevo)"),
}
}
}

Salida:

"alice" -> "alice" (prestado, sin copia)
" Bob " -> "bob" (propio, String nuevo)
"CAROL" -> "carol" (propio, String nuevo)
"dave" -> "dave" (prestado, sin copia)
▶ Pruébalo tú mismo

Dos de los cuatro nombres no cuestan nada. En un programa real en el que el 99 % de la entrada ya está limpia, son muchísimas copias que te ahorras. El '_ de Cow<'_, str> solo dice “esto puede tomar prestado de la entrada”. Si los lifetimes todavía te resultan confusos, la página de lifetimes los explica.

Normalmente no necesitas hacer match sobre un Cow. Hace deref a &str, así que puedes llamar a métodos de cadenas de texto sobre él y pasar &name a cualquier función que reciba &str. Cuando de verdad necesites un String propio, llama a .into_owned(). Si dentro hay un String, lo saca tal cual, y solo copia si el Cow era prestado.

use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> {
let trimmed = name.trim();
if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) {
Cow::Borrowed(name)
} else {
Cow::Owned(trimmed.to_lowercase())
}
}
fn greet(name: &str) {
println!("¡Bienvenida, {name}!");
}
fn main() {
let name = normalize(" Eve ");
println!("longitud: {}", name.len()); // los métodos de str funcionan directamente
println!("empieza por e: {}", name.starts_with('e'));
greet(&name); // &Cow<str> se convierte en &str
let saved: String = name.into_owned(); // ahora es un String normal
println!("{saved:?} guardado en la lista de usuarios");
}

Salida:

longitud: 3
empieza por e: true
¡Bienvenida, eve!
"eve" guardado en la lista de usuarios
▶ Pruébalo tú mismo

Cow también funciona con otros tipos, como Cow<'_, [i32]> para un slice que quizá haya que cambiar. Pero para texto, Cow<'_, str> es el que más verás.

Aquí tienes una pequeña chuleta con todos los tipos parecidos a punteros que has visto hasta ahora:

Tipo Qué te da Úsalo cuando
Box<T> Un solo dueño, valor en el heap Tipos recursivos, o un valor grande que quieres mover sin coste
Rc<T> Muchos dueños, un solo hilo Varias partes de tu programa comparten los mismos datos
Arc<T> Muchos dueños, entre hilos Igual que Rc, pero compartido entre hilos (consulta Estado compartido)
Weak<T> Un enlace que no es dueño Enlaces al padre, listas de observadores, cachés, romper ciclos de Rc
RefCell<T> Mutación a través de una referencia compartida, comprobada en tiempo de ejecución Necesitas cambiar algo que vive dentro de un Rc
Cow<'_, T> Prestado o propio, decidido en tiempo de ejecución Una función que normalmente devuelve su entrada sin cambios

Si no estás seguro, empieza con ownership (propiedad) y borrowing (préstamo) normales. Añade estos tipos solo cuando el compilador, o una fuga de memoria, te diga que los necesitas.

Hacer unwrap de un upgrade cuando el valor ya no existe

Sección titulada «Hacer unwrap de un upgrade cuando el valor ya no existe»

upgrade() devuelve un Option por algo. Si le haces .unwrap() por costumbre, tu programa provocará un panic en cuanto el valor se haya liberado:

use std::rc::Rc;
fn main() {
let session = Rc::new(String::from("usuario-42"));
let link = Rc::downgrade(&session);
drop(session); // la sesión termina
let current = link.upgrade().unwrap();
println!("sigues conectado como {current}");
}

Al ejecutarlo, obtienes:

thread 'main' (12345) panicked at src/main.rs:9:34:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Pruébalo tú mismo

El mensaje dice que se llamó a Option::unwrap() sobre un valor None, y te indica la línea exacta. La solución es tratar “ya no existe” como un caso normal, con match o if let:

match link.upgrade() {
Some(current) => println!("sigues conectado como {current}"),
None => println!("la sesión ha terminado"),
}

Usar Rc para el enlace que apunta hacia atrás

Sección titulada «Usar Rc para el enlace que apunta hacia atrás»

Si un hijo guarda Rc<Parent> y el padre guarda Rc<Child>, has construido el ciclo de la primera sección. Nada falla y nada te avisa; la memoria simplemente nunca vuelve. Una buena regla: decide quién es dueño de quién, usa Rc en esa dirección y Weak para todos los enlaces que van en la otra.

struct Folder {
parent: RefCell<Rc<Folder>>, // mal: el hijo mantiene vivo al padre
children: RefCell<Vec<Rc<Folder>>>,
}

Cambia parent a RefCell<Weak<Folder>> y el ciclo desaparece.

1. Comentarios en una publicación eliminada

Sección titulada «1. Comentarios en una publicación eliminada»

Crea un Post con un title, guardado en un Rc. Crea un Comment con un text y un post: Weak<Post>. Escribe una función describe(comment: &Comment) que imprima "<texto>" en <título> si la publicación sigue existiendo, o "<texto>" en una publicación eliminada si no. Llámala, libera la publicación y vuelve a llamarla. Pista: usa match comment.post.upgrade().

Ver solución
use std::rc::{Rc, Weak};
struct Post {
title: String,
}
struct Comment {
text: String,
post: Weak<Post>,
}
fn describe(comment: &Comment) {
match comment.post.upgrade() {
Some(post) => println!("\"{}\" en {}", comment.text, post.title),
None => println!("\"{}\" en una publicación eliminada", comment.text),
}
}
fn main() {
let post = Rc::new(Post { title: String::from("Aprendiendo Rust") });
let comment = Comment { text: String::from("¡Muy buena publicación!"), post: Rc::downgrade(&post) };
describe(&comment);
drop(post);
describe(&comment);
}

Salida:

"¡Muy buena publicación!" en Aprendiendo Rust
"¡Muy buena publicación!" en una publicación eliminada
▶ Pruébalo tú mismo

El comentario nunca mantiene viva la publicación. En cuanto se libera el único Rc, upgrade() devuelve None y el comentario lo maneja con calma en lugar de provocar un panic.

Escribe fn escape(text: &str) -> Cow<'_, str> que sustituya cada < por &lt;. Si el texto no tiene ningún <, devuélvelo prestado. Pruébala con "hola mundo" y "1 < 2", e imprime qué variante obtuviste. Pista: compruébalo primero con text.contains('<') y luego usa text.replace('<', "&lt;") para el caso propio.

Ver solución
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} else {
Cow::Borrowed(text)
}
}
fn main() {
for text in ["hola mundo", "1 < 2"] {
match escape(text) {
Cow::Borrowed(s) => println!("{s} (prestado)"),
Cow::Owned(s) => println!("{s} (propio)"),
}
}
}

Salida:

hola mundo (prestado)
1 &lt; 2 (propio)
▶ Pruébalo tú mismo

Solo el texto que de verdad contiene < paga por un String nuevo. Todo lo demás pasa directamente, sin tocarlo.

  • Dos valores Rc que se apuntan entre sí nunca llegan a un contador fuerte de 0, así que se pierden (fuga de memoria) y su Drop nunca se ejecuta.
  • Rc::downgrade crea un Weak<T>, que no mantiene vivo el valor. upgrade() te devuelve un Option<Rc<T>>.
  • En los árboles, los hijos se poseen con Rc y los enlaces al padre son Weak. La propiedad baja, los enlaces débiles apuntan hacia arriba.
  • Las listas de observadores y las cachés pueden guardar Weak para no mantener nunca nada vivo por accidente.
  • Cow<'_, str> devuelve texto prestado cuando nada cambió, y un String propio solo cuando sí cambió. Hace deref a &str, y .into_owned() te da un String.

Comprueba lo aprendido

1. Dos valores guardan cada uno un Rc al otro. ¿Qué pasa cuando termina main?

2. ¿Qué devuelve weak.upgrade()?

3. En un árbol de carpetas, ¿qué enlace debería ser Weak?

4. ¿Cuándo evita una función que devuelve Cow<'_, str> crear un String nuevo?

Puntuación: 0 / 4