Introducción
Bienvenido a Usando Box
En esta práctica, aprenderemos a usar los punteros inteligentes Box
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 Usando Box
En esta práctica, aprenderemos a usar los punteros inteligentes Box
<T>
{=html} para apuntar a datos en el montónEl puntero inteligente más sencillo es una caja, cuyo tipo se escribe Box<T>
. Las cajas te permiten almacenar datos en el montón en lugar de la pila. Lo que queda en la pila es el puntero a los datos del montón. Consulte el Capítulo 4 para revisar la diferencia entre la pila y el montón.
Las cajas no tienen sobrecarga de rendimiento, aparte de almacenar sus datos en el montón en lugar de la pila. Pero tampoco tienen muchas capacidades adicionales. Las usarás con más frecuencia en estas situaciones:
Demostraremos la primera situación en "Habilitando tipos recursivos con cajas". En el segundo caso, transferir la propiedad de una gran cantidad de datos puede llevar mucho tiempo porque los datos se copian por la pila. Para mejorar el rendimiento en esta situación, podemos almacenar la gran cantidad de datos en el montón en una caja. Luego, solo la pequeña cantidad de datos de puntero se copia por la pila, mientras que los datos a los que hace referencia permanecen en un lugar del montón. El tercer caso se conoce como objeto de trato, y "Usando objetos de trato que permiten valores de diferentes tipos" se dedica a ese tema. ¡Así que lo que aprendes aquí lo aplicarás nuevamente en esa sección!
<T>
{=html} para almacenar datos en el montónAntes de discutir el caso de uso de almacenamiento en el montón para Box<T>
, cubriremos la sintaxis y cómo interactuar con los valores almacenados dentro de un Box<T>
.
La Lista 15-1 muestra cómo usar una caja para almacenar un valor de i32
en el montón.
Nombre de archivo: src/main.rs
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
Lista 15-1: Almacenando un valor de i32
en el montón usando una caja
Definimos la variable b
para que tenga el valor de una Caja
que apunta al valor 5
, que se asigna en el montón. Este programa imprimirá b = 5
; en este caso, podemos acceder a los datos en la caja de manera similar a como lo haríamos si estos datos estuvieran en la pila. Al igual que cualquier valor poseído, cuando una caja sale del ámbito, como lo hace b
al final de main
, se desasignará. La desasignación ocurre tanto para la caja (almacenada en la pila) como para los datos a los que apunta (almacenados en el montón).
Poner un solo valor en el montón no es muy útil, por lo que no usarás cajas por sí mismas de esta manera muy a menudo. Tener valores como un solo i32
en la pila, donde se almacenan por defecto, es más adecuado en la mayoría de las situaciones. Veamos un caso en el que las cajas nos permiten definir tipos que no podríamos definir si no tuviéramos cajas.
Un valor de un tipo recursivo puede tener otro valor del mismo tipo como parte de sí mismo. Los tipos recursivos plantean un problema porque en tiempo de compilación Rust necesita saber cuánto espacio ocupa un tipo. Sin embargo, el anidamiento de valores de tipos recursivos podría teóricamente continuar indefinidamente, por lo que Rust no puede saber cuánto espacio necesita el valor. Debido a que las cajas tienen un tamaño conocido, podemos habilitar tipos recursivos insertando una caja en la definición del tipo recursivo.
Como ejemplo de un tipo recursivo, exploremos la lista cons. Este es un tipo de datos común en los lenguajes de programación funcionales. El tipo de lista cons que definiremos es sencillo excepto por la recursividad; por lo tanto, los conceptos en el ejemplo con el que trabajaremos serán útiles en cualquier momento que te enfrentes a situaciones más complejas que involucren tipos recursivos.
Una lista cons es una estructura de datos que proviene del lenguaje de programación Lisp y sus dialectos, está compuesta por pares anidados y es la versión de Lisp de una lista enlazada. Su nombre proviene de la función cons
(abreviatura de función constructora) en Lisp que construye un nuevo par a partir de sus dos argumentos. Al llamar a cons
en un par que consta de un valor y otro par, podemos construir listas cons compuestas por pares recursivos.
Por ejemplo, aquí está una representación en pseudocódigo de una lista cons que contiene la lista 1, 2, 3
con cada par en paréntesis:
(1, (2, (3, Nil)))
Cada elemento en una lista cons contiene dos elementos: el valor del elemento actual y el siguiente elemento. El último elemento de la lista contiene solo un valor llamado Nil
sin un siguiente elemento. Una lista cons se produce llamando recursivamente a la función cons
. El nombre canónico para denotar el caso base de la recursividad es Nil
. Tenga en cuenta que esto no es lo mismo que el concepto de "nulo" o "nil" del Capítulo 6, que es un valor inválido o ausente.
La lista cons no es una estructura de datos comúnmente utilizada en Rust. En la mayoría de los casos, cuando tienes una lista de elementos en Rust, Vec<T>
es una mejor opción para usar. Otros tipos de datos recursivos más complejos son útiles en diversas situaciones, pero al comenzar con la lista cons en este capítulo, podemos explorar cómo las cajas nos permiten definir un tipo de datos recursivo sin muchas distracciones.
La Lista 15-2 contiene una definición de enumerado para una lista cons. Tenga en cuenta que este código no se compilará todavía porque el tipo List
no tiene un tamaño conocido, lo que demostraremos.
Nombre de archivo: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
Lista 15-2: El primer intento de definir un enumerado para representar una estructura de datos de lista cons de valores de i32
Nota: Estamos implementando una lista cons que solo contiene valores de
i32
con fines de este ejemplo. Podríamos haberla implementado usando genéricos, como discutimos en el Capítulo 10, para definir un tipo de lista cons que podría almacenar valores de cualquier tipo.
Usar el tipo List
para almacenar la lista 1, 2, 3
se vería como el código de la Lista 15-3.
Nombre de archivo: src/main.rs
--snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Lista 15-3: Usando el enumerado List
para almacenar la lista 1, 2, 3
El primer valor Cons
contiene 1
y otro valor de tipo List
. Este valor de tipo List
es otro valor Cons
que contiene 2
y otro valor de tipo List
. Este valor de tipo List
es otro valor Cons
que contiene 3
y un valor de tipo List
, que finalmente es Nil
, la variante no recursiva que indica el final de la lista.
Si intentamos compilar el código de la Lista 15-3, obtenemos el error mostrado en la Lista 15-4.
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
|
2 | Cons(i32, Box<List>),
| ++++ +
Lista 15-4: El error que obtenemos al intentar definir un enumerado recursivo
El error muestra que este tipo "tiene un tamaño infinito". La razón es que hemos definido List
con una variante que es recursiva: contiene directamente otro valor de sí mismo. Como resultado, Rust no puede determinar cuánto espacio necesita para almacenar un valor de tipo List
. Vamos a analizar por qué obtenemos este error. Primero veremos cómo Rust decide cuánto espacio necesita para almacenar un valor de un tipo no recursivo.
Recuerde el enumerado Message
que definimos en la Lista 6-2 cuando discutimos las definiciones de enumerados en el Capítulo 6:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Para determinar cuánto espacio asignar para un valor de Message
, Rust examina cada una de las variantes para ver cuál necesita más espacio. Rust ve que Message::Quit
no necesita ningún espacio, Message::Move
necesita suficiente espacio para almacenar dos valores de i32
, y así sucesivamente. Debido a que solo se usará una variante, el mayor espacio que un valor de Message
necesitará es el espacio que ocuparía almacenar la más grande de sus variantes.
Contraste esto con lo que sucede cuando Rust intenta determinar cuánto espacio necesita un tipo recursivo como el enumerado List
en la Lista 15-2. El compilador comienza examinando la variante Cons
, que contiene un valor de tipo i32
y un valor de tipo List
. Por lo tanto, Cons
necesita una cantidad de espacio igual al tamaño de un i32
más el tamaño de un List
. Para determinar cuánta memoria necesita el tipo List
, el compilador examina las variantes, comenzando por la variante Cons
. La variante Cons
contiene un valor de tipo i32
y un valor de tipo List
, y este proceso continúa indefinidamente, como se muestra en la Figura 15-1.
Figura 15-1: Una List
infinita compuesta por variantes Cons
infinitas
<T>
{=html} para obtener un tipo recursivo con un tamaño conocidoDebido a que Rust no puede determinar cuánto espacio asignar para tipos definidos recursivamente, el compilador muestra un error con esta sugerencia útil:
ayuda: insertar alguna indirección (por ejemplo, un `Box`, `Rc` o `&`) para que `List` sea representable
|
2 | Cons(i32, Box<List>),
| ++++ +
En esta sugerencia, indirección significa que en lugar de almacenar un valor directamente, debemos cambiar la estructura de datos para almacenar el valor indirectamente mediante el almacenamiento de un puntero al valor en lugar de hacerlo directamente.
Debido a que un Box<T>
es un puntero, Rust siempre sabe cuánto espacio necesita un Box<T>
: el tamaño de un puntero no cambia en función de la cantidad de datos a los que apunta. Esto significa que podemos poner un Box<T>
dentro de la variante Cons
en lugar de otro valor de tipo List
directamente. El Box<T>
apuntará al siguiente valor de tipo List
que estará en el montón en lugar de estar dentro de la variante Cons
. Conceptualmente, todavía tenemos una lista, creada con listas que contienen otras listas, pero esta implementación ahora es más como colocar los elementos uno al lado del otro en lugar de dentro de uno otro.
Podemos cambiar la definición del enumerado List
en la Lista 15-2 y el uso del List
en la Lista 15-3 al código de la Lista 15-5, que se compilará.
Nombre de archivo: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(
1,
Box::new(Cons(
2,
Box::new(Cons(
3,
Box::new(Nil)
))
))
);
}
Lista 15-5: Definición de List
que utiliza Box<T>
para tener un tamaño conocido
La variante Cons
necesita el tamaño de un i32
más el espacio para almacenar los datos del puntero de la caja. La variante Nil
no almacena ningún valor, por lo que necesita menos espacio que la variante Cons
. Ahora sabemos que cualquier valor de tipo List
ocupará el tamaño de un i32
más el tamaño de los datos del puntero de una caja. Al usar una caja, hemos roto la cadena infinita y recursiva, por lo que el compilador puede determinar el tamaño que necesita para almacenar un valor de tipo List
. La Figura 15-2 muestra cómo se ve ahora la variante Cons
.
Figura 15-2: Una List
que no tiene un tamaño infinito, porque Cons
contiene un Box
Las cajas solo proporcionan la indirección y la asignación de memoria en el montón; no tienen ninguna otra capacidad especial, como las que veremos con los otros tipos de punteros inteligentes. Tampoco tienen la sobrecarga de rendimiento que incurren estas capacidades especiales, por lo que pueden ser útiles en casos como la lista cons donde la indirección es la única característica que necesitamos. Veremos más casos de uso de cajas en el Capítulo 17.
El tipo Box<T>
es un puntero inteligente porque implementa el trato Deref
, que permite tratar los valores de Box<T>
como referencias. Cuando un valor de Box<T>
sale del ámbito, los datos del montón a los que apunta la caja también se limpian debido a la implementación del trato Drop
. Estos dos tratados serán aún más importantes para la funcionalidad proporcionada por los otros tipos de punteros inteligentes que discutiremos en el resto de este capítulo. Exploremos estos dos tratados con más detalle.
¡Felicidades! Has completado el laboratorio de Uso de Box