Creando un Ciclo de Referencia
Veamos cómo podría ocurrir un ciclo de referencia y cómo evitarlo, comenzando con la definición del enum List
y un método tail
en la Lista 15-25.
Nombre de archivo: src/main.rs
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
1 Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
2 fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
Lista 15-25: Definición de una lista cons que contiene un RefCell<T>
para que podamos modificar a qué se refiere una variante Cons
Estamos usando otra variación de la definición de List
de la Lista 15-5. El segundo elemento en la variante Cons
ahora es RefCell<Rc<List>>
[1], lo que significa que en lugar de tener la capacidad de modificar el valor i32
como hicimos en la Lista 15-24, queremos modificar el valor de List
al que apunta una variante Cons
. También estamos agregando un método tail
[2] para que sea conveniente acceder al segundo elemento si tenemos una variante Cons
.
En la Lista 15-26, estamos agregando una función main
que utiliza las definiciones de la Lista 15-25. Este código crea una lista en a
y una lista en b
que apunta a la lista en a
. Luego modifica la lista en a
para que apunte a b
, creando un ciclo de referencia. Hay declaraciones println!
a lo largo del camino para mostrar cuáles son los recuentos de referencias en varios puntos de este proceso.
Nombre de archivo: src/main.rs
fn main() {
1 let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
2 let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!(
"a rc count after b creation = {}",
Rc::strong_count(&a)
);
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
3 if let Some(link) = a.tail() {
4 *link.borrow_mut() = Rc::clone(&b);
}
println!(
"b rc count after changing a = {}",
Rc::strong_count(&b)
);
println!(
"a rc count after changing a = {}",
Rc::strong_count(&a)
);
// Descomenta la siguiente línea para ver que tenemos un ciclo;
// desbordará la pila
// println!("a next item = {:?}", a.tail());
}
Lista 15-26: Creando un ciclo de referencia de dos valores List
que se apuntan mutuamente
Creamos una instancia Rc<List>
que contiene un valor de List
en la variable a
con una lista inicial de 5, Nil
[1]. Luego creamos una instancia Rc<List>
que contiene otro valor de List
en la variable b
que contiene el valor 10
y apunta a la lista en a
[2].
Modificamos a
para que apunte a b
en lugar de Nil
, creando un ciclo. Hacemos eso usando el método tail
para obtener una referencia al RefCell<Rc<List>>
en a
, que ponemos en la variable link
[3]. Luego usamos el método borrow_mut
en el RefCell<Rc<List>>
para cambiar el valor interno de un Rc<List>
que contiene un valor Nil
al Rc<List>
en b
[4].
Cuando ejecutamos este código, manteniendo la última declaración println!
comentada por el momento, obtendremos esta salida:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
El recuento de referencias de las instancias Rc<List>
en tanto a
como b
es 2 después de cambiar la lista en a
para que apunte a b
. Al final de main
, Rust elimina la variable b
, lo que reduce el recuento de referencias de la instancia Rc<List>
de b
de 2 a 1. La memoria que tiene Rc<List>
en el montón no se eliminará en este punto porque su recuento de referencias es 1, no 0. Luego Rust elimina a
, lo que también reduce el recuento de referencias de la instancia Rc<List>
de a
de 2 a 1. La memoria de esta instancia tampoco se puede eliminar, porque la otra instancia Rc<List>
todavía la referencia. La memoria asignada a la lista permanecerá sin recopilar para siempre. Para visualizar este ciclo de referencia, hemos creado un diagrama en la Figura 15-4.
Figura 15-4: Un ciclo de referencia de listas a
y b
que se apuntan mutuamente
Si descomentas la última declaración println!
y ejecutas el programa, Rust intentará imprimir este ciclo con a
apuntando a b
apuntando a a
y así sucesivamente hasta que desborde la pila.
En comparación con un programa del mundo real, las consecuencias de crear un ciclo de referencia en este ejemplo no son muy graves: justo después de crear el ciclo de referencia, el programa termina. Sin embargo, si un programa más complejo asignara mucha memoria en un ciclo y la mantuviera durante mucho tiempo, el programa usaría más memoria de la necesaria y podría abrumar el sistema, causando que se agote la memoria disponible.
Crear ciclos de referencia no es fácil de hacer, pero tampoco es imposible. Si tienes valores RefCell<T>
que contienen valores Rc<T>
o combinaciones anidadas similares de tipos con mutabilidad interior y conteo de referencias, debes asegurarte de no crear ciclos; no puedes confiar en que Rust los capture. Crear un ciclo de referencia sería un error lógico en tu programa que debes minimizar utilizando pruebas automatizadas, revisiones de código y otras prácticas de desarrollo de software.
Otra solución para evitar los ciclos de referencia es reorganizar tus estructuras de datos de modo que algunas referencias expresen propiedad y otras no. Como resultado, puedes tener ciclos formados por algunas relaciones de propiedad y algunas relaciones no de propiedad, y solo las relaciones de propiedad afectan a si un valor puede ser eliminado. En la Lista 15-25, siempre queremos que las variantes Cons
posean su lista, por lo que no es posible reorganizar la estructura de datos. Veamos un ejemplo usando gráficos compuestos por nodos padre y nodos hijo para ver cuándo las relaciones no de propiedad son una forma adecuada de prevenir ciclos de referencia.