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: 2contador fuerte de bob: 2fin de mainFí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 bobalice: fuertes = 1, débiles = 1fin de mainliberando a bobliberando a aliceEsta 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 devuelveNone. Es un valor inicial muy práctico.Rc::strong_countcuenta los dueños.Rc::weak_countcuenta los enlaces débiles. Solo el contador fuerte decide cuándo se libera el valor.- El
Rcque recibes deupgrade()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 homeruta completa: home/fotos/viajeshome: fuertes = 1, débiles = 1fotos: fuertes = 2, débiles = 1Lee 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 10registro recibió: la puntuación es 10pantalla recibió: la puntuación es 20(un listener ya no existe, se omite)listeners restantes: 1La 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 unStringnuevo”.
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)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.
Usar un Cow como un string normal
Sección titulada «Usar un Cow como un string normal»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: 3empieza por e: true¡Bienvenida, eve!"eve" guardado en la lista de usuariosCow 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.
Cuándo usar cada uno
Sección titulada «Cuándo usar cada uno»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.
Errores comunes
Sección titulada «Errores comunes»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` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceEl 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.
Práctica
Sección titulada «Práctica»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 eliminadaEl 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.
2. Escapar texto solo cuando hace falta
Sección titulada «2. Escapar texto solo cuando hace falta»Escribe fn escape(text: &str) -> Cow<'_, str> que sustituya cada < por <. 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('<', "<") para el caso propio.
Ver solución
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } 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 < 2 (propio)Solo el texto que de verdad contiene < paga por un String nuevo. Todo lo demás pasa directamente, sin tocarlo.
Resumen
Sección titulada «Resumen»- Dos valores
Rcque se apuntan entre sí nunca llegan a un contador fuerte de 0, así que se pierden (fuga de memoria) y suDropnunca se ejecuta. Rc::downgradecrea unWeak<T>, que no mantiene vivo el valor.upgrade()te devuelve unOption<Rc<T>>.- En los árboles, los hijos se poseen con
Rcy los enlaces al padre sonWeak. La propiedad baja, los enlaces débiles apuntan hacia arriba. - Las listas de observadores y las cachés pueden guardar
Weakpara no mantener nunca nada vivo por accidente. Cow<'_, str>devuelve texto prestado cuando nada cambió, y unStringpropio solo cuando sí cambió. Hace deref a&str, y.into_owned()te da unString.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Los ciclos de referencias pueden provocar fugas de memoria — El libro de Rust (en inglés): la explicación oficial de los ciclos de
Rcy de un árbol con enlacesWeakal padre. std::rc::Weak— documentación de la biblioteca estándar (en inglés): todos los métodos deWeak, incluidosupgrade,strong_countyWeak::new.std::rc— documentación de la biblioteca estándar (en inglés): cómo funcionan juntos los contadores fuertes y débiles, con un ejemplo más largo de cómo romper ciclos.std::borrow::Cow— documentación de la biblioteca estándar (en inglés): el enumCow,to_mut,into_ownedy ejemplos más allá de los strings.