Introducción
Bienvenido a Rc
En esta práctica, exploraremos el uso de Rc
This tutorial is from open-source community. Access the source code
💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí
Bienvenido a Rc
En esta práctica, exploraremos el uso de Rc
<T>
{=html}, el puntero inteligente con conteo de referenciasEn la mayoría de los casos, la propiedad es clara: sabes exactamente qué variable posee un valor dado. Sin embargo, hay casos en los que un solo valor puede tener múltiples propietarios. Por ejemplo, en las estructuras de datos de gráficos, múltiples aristas pueden apuntar al mismo nodo, y ese nodo es conceptualmente propiedad de todas las aristas que apuntan a él. Un nodo no debe ser eliminado a menos que no tenga ninguna arista apuntando a él y, por lo tanto, no tenga propietarios.
Tienes que habilitar la propiedad múltiple explícitamente mediante el uso del tipo Rust Rc<T>
, que es una abreviatura de conteo de referencias. El tipo Rc<T>
lleva un registro del número de referencias a un valor para determinar si el valor todavía está en uso. Si no hay referencias a un valor, el valor se puede eliminar sin que ninguna referencia quede invalida.
Imagina Rc<T>
como un televisor en una sala familiar. Cuando una persona entra a ver la televisión, la enciende. Otros pueden entrar a la habitación y ver la televisión. Cuando la última persona sale de la habitación, apaga el televisor porque ya no se está utilizando. Si alguien apaga el televisor mientras otros todavía están viéndolo, habría un alboroto de los demás espectadores de televisión.
Usamos el tipo Rc<T>
cuando queremos asignar algunos datos en el montón para que múltiples partes de nuestro programa los lean y no podemos determinar en tiempo de compilación qué parte terminará usando los datos por última vez. Si supiéramos qué parte terminaría última, simplemente podríamos hacer que esa parte fuera la propietaria de los datos, y las reglas normales de propiedad aplicadas en tiempo de compilación entraría en vigor.
Tenga en cuenta que Rc<T>
solo se puede usar en escenarios de un solo subproceso. Cuando discutamos la concurrencia en el Capítulo 16, cubriremos cómo hacer conteo de referencias en programas multihilo.
<T>
{=html} para compartir datosVolvamos a nuestro ejemplo de lista enlazada en la Lista 15-5. Recuerde que la definimos usando Box<T>
. Esta vez, crearemos dos listas que comparten la propiedad de una tercera lista. Conceptualmente, esto se parece a la Figura 15-3.
Figura 15-3: Dos listas, b
y c
, compartiendo la propiedad de una tercera lista, a
Crearemos la lista a
que contiene 5
y luego 10
. Luego crearemos dos listas más: b
que comienza con 3
y c
que comienza con 4
. Ambas listas b
y c
luego continuarán con la primera lista a
que contiene 5
y 10
. En otras palabras, ambas listas compartirán la primera lista que contiene 5
y 10
.
Intentar implementar este escenario usando nuestra definición de List
con Box<T>
no funcionará, como se muestra en la Lista 15-17.
Nombre de archivo: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
1 let b = Cons(3, Box::new(a));
2 let c = Cons(4, Box::new(a));
}
Lista 15-17: Demostrando que no se permite tener dos listas usando Box<T>
que intentan compartir la propiedad de una tercera lista
Cuando compilamos este código, obtenemos este error:
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which
does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
Las variantes Cons
poseen los datos que contienen, por lo que cuando creamos la lista b
[1], a
se mueve a b
y b
posee a
. Luego, cuando intentamos usar a
nuevamente al crear c
[2], no se nos permite hacerlo porque a
ha sido movido.
Podríamos cambiar la definición de Cons
para que tenga referencias en lugar de valores, pero entonces tendríamos que especificar parámetros de vida. Al especificar parámetros de vida, estaríamos especificando que cada elemento de la lista vivirá al menos tanto tiempo como la lista completa. Este es el caso para los elementos y listas de la Lista 15-17, pero no en todos los escenarios.
En lugar de eso, cambiaremos nuestra definición de List
para usar Rc<T>
en lugar de Box<T>
, como se muestra en la Lista 15-18. Cada variante Cons
ahora contendrá un valor y un Rc<T>
que apunta a una List
. Cuando creamos b
, en lugar de tomar posesión de a
, clonaremos el Rc<List>
que a
está manteniendo, aumentando así el número de referencias de uno a dos y permitiendo que a
y b
compartan la propiedad de los datos en ese Rc<List>
. También clonaremos a
al crear c
, aumentando el número de referencias de dos a tres. Cada vez que llamamos a Rc::clone
, el recuento de referencias al data dentro del Rc<List>
aumentará, y los datos no se limpiarán a menos que no haya referencias a ellos.
Nombre de archivo: src/main.rs
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
1 use std::rc::Rc;
fn main() {
2 let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
3 let b = Cons(3, Rc::clone(&a));
4 let c = Cons(4, Rc::clone(&a));
}
Lista 15-18: Una definición de List
que usa Rc<T>
Necesitamos agregar una declaración use
para traer Rc<T>
al ámbito [1] porque no está en el preludio. En main
, creamos la lista que contiene 5
y 10
y la almacenamos en un nuevo Rc<List>
en a
[2]. Luego, cuando creamos b
[3] y c
[4], llamamos a la función Rc::clone
y pasamos una referencia al Rc<List>
en a
como argumento.
Podríamos haber llamado a.clone()
en lugar de Rc::clone(&a)
, pero la convención de Rust es usar Rc::clone
en este caso. La implementación de Rc::clone
no hace una copia profunda de todos los datos como lo hacen la mayoría de las implementaciones de clone
de los tipos. La llamada a Rc::clone
solo incrementa el recuento de referencias, lo que no lleva mucho tiempo. Las copias profundas de datos pueden llevar mucho tiempo. Al usar Rc::clone
para el conteo de referencias, podemos distinguir visualmente entre los tipos de clonación de copia profunda y los tipos de clonación que aumentan el recuento de referencias. Al buscar problemas de rendimiento en el código, solo necesitamos considerar las clonaciones de copia profunda y podemos ignorar las llamadas a Rc::clone
.
<T>
{=html} aumenta el recuento de referenciasModifiquemos nuestro ejemplo funcional de la Lista 15-18 para ver cómo cambian los recuentos de referencias a medida que creamos y eliminamos referencias al Rc<List>
en a
.
En la Lista 15-19, modificaremos main
para que tenga un ámbito interno alrededor de la lista c
; luego podemos ver cómo cambia el recuento de referencias cuando c
sale del ámbito.
Nombre de archivo: src/main.rs
--snip--
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!(
"count after creating a = {}",
Rc::strong_count(&a)
);
let b = Cons(3, Rc::clone(&a));
println!(
"count after creating b = {}",
Rc::strong_count(&a)
);
{
let c = Cons(4, Rc::clone(&a));
println!(
"count after creating c = {}",
Rc::strong_count(&a)
);
}
println!(
"count after c goes out of scope = {}",
Rc::strong_count(&a)
);
}
Lista 15-19: Imprimiendo el recuento de referencias
En cada punto del programa en el que el recuento de referencias cambia, imprimimos el recuento de referencias, que obtenemos llamando a la función Rc::strong_count
. Esta función se llama strong_count
en lugar de count
porque el tipo Rc<T>
también tiene un weak_count
; veremos para qué se utiliza weak_count
en "Evitando ciclos de referencias usando Weak<T>
{=html}".
Este código imprime lo siguiente:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
Podemos ver que el Rc<List>
en a
tiene un recuento de referencias inicial de 1; luego, cada vez que llamamos a clone
, el recuento aumenta en 1. Cuando c
sale del ámbito, el recuento disminuye en 1. No tenemos que llamar a una función para disminuir el recuento de referencias como tenemos que llamar a Rc::clone
para aumentar el recuento de referencias: la implementación del trato Drop
disminuye el recuento de referencias automáticamente cuando un valor Rc<T>
sale del ámbito.
Lo que no podemos ver en este ejemplo es que cuando b
y luego a
salen del ámbito al final de main
, el recuento es entonces 0, y el Rc<List>
se limpia completamente. Usar Rc<T>
permite que un solo valor tenga múltiples propietarios, y el recuento garantiza que el valor siga siendo válido mientras exista cualquiera de los propietarios.
A través de referencias inmutables, Rc<T>
te permite compartir datos entre múltiples partes de tu programa solo para lectura. Si Rc<T>
te permitiera tener también múltiples referencias mutables, es posible que violaras una de las reglas de préstamo discutidas en el Capítulo 4: múltiples préstamos mutables al mismo lugar pueden causar conflictos de datos e inconsistencias. Pero poder mutar los datos es muy útil! En la siguiente sección, discutiremos el patrón de mutabilidad interior y el tipo RefCell<T>
que puedes usar en combinación con un Rc<T>
para trabajar con esta restricción de inmutabilidad.
¡Felicidades! Has completado la práctica de Rc