Almacenar listas de valores con vectores

RustRustBeginner
Practicar Ahora

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í

Introducción

Bienvenido a Almacenar listas de valores con vectores. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

"En esta práctica, exploraremos el tipo de colección Vec<T>, también conocido como vector, que permite almacenar listas de valores del mismo tipo en una sola estructura de datos."


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/floating_types("Floating-point Types") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/mutable_variables -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/integer_types -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/floating_types -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/for_loop -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/expressions_statements -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/method_syntax -.-> lab-100406{{"Almacenar listas de valores con vectores"}} rust/operator_overloading -.-> lab-100406{{"Almacenar listas de valores con vectores"}} end

Almacenar listas de valores con vectores

El primer tipo de colección que veremos es Vec<T>, también conocido como vector. Los vectores te permiten almacenar más de un valor en una sola estructura de datos que coloca todos los valores uno al lado del otro en la memoria. Los vectores solo pueden almacenar valores del mismo tipo. Son útiles cuando tienes una lista de elementos, como las líneas de texto en un archivo o los precios de los artículos en un carrito de compras.

Crear un nuevo vector

Para crear un nuevo vector vacío, llamamos a la función Vec::new, como se muestra en la Lista 8-1.

let v: Vec<i32> = Vec::new();

Lista 8-1: Crear un nuevo vector vacío para almacenar valores del tipo i32

Tenga en cuenta que agregamos una anotación de tipo aquí. Dado que no estamos insertando ningún valor en este vector, Rust no sabe de qué tipo de elementos pretendemos almacenar. Este es un punto importante. Los vectores se implementan utilizando genéricos; cubriremos cómo usar genéricos con sus propios tipos en el Capítulo 10. Por ahora, sepa que el tipo Vec<T> proporcionado por la biblioteca estándar puede almacenar cualquier tipo. Cuando creamos un vector para almacenar un tipo específico, podemos especificar el tipo dentro de los corchetes angulares. En la Lista 8-1, le hemos dicho a Rust que el Vec<T> en v contendrá elementos del tipo i32.

Con más frecuencia, creará un Vec<T> con valores iniciales y Rust inferirá el tipo de valor que desea almacenar, por lo que rara vez necesitará hacer esta anotación de tipo. Rust convenientemente proporciona la macro vec!, que creará un nuevo vector que contendrá los valores que le des. La Lista 8-2 crea un nuevo Vec<i32> que contiene los valores 1, 2 y 3. El tipo entero es i32 porque ese es el tipo entero predeterminado, como discutimos en "Tipos de datos".

let v = vec![1, 2, 3];

Lista 8-2: Crear un nuevo vector que contiene valores

Dado que hemos dado valores iniciales de i32, Rust puede inferir que el tipo de v es Vec<i32>, y la anotación de tipo no es necesaria. A continuación, veremos cómo modificar un vector.

Actualizar un vector

Para crear un vector y luego agregar elementos a él, podemos usar el método push, como se muestra en la Lista 8-3.

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

Lista 8-3: Usar el método push para agregar valores a un vector

Como con cualquier variable, si queremos poder cambiar su valor, necesitamos hacerla mutable usando la palabra clave mut, como se discutió en el Capítulo 3. Los números que ponemos dentro son todos del tipo i32, y Rust infiere esto a partir de los datos, por lo que no necesitamos la anotación Vec<i32>.

Leer elementos de vectores

Hay dos maneras de referirse a un valor almacenado en un vector: a través de la indexación o mediante el uso del método get. En los siguientes ejemplos, hemos anotado los tipos de los valores que se devuelven de estas funciones para mayor claridad.

La Lista 8-4 muestra ambos métodos de acceder a un valor en un vector, con la sintaxis de indexación y el método get.

let v = vec![1, 2, 3, 4, 5];

1 let third: &i32 = &v[2];
println!("The third element is {third}");

2 let third: Option<&i32> = v.get(2);
match third  {
    Some(third) => println!("The third element is {third}"),
    None => println!("There is no third element."),
}

Lista 8-4: Usar la sintaxis de indexación y el método get para acceder a un elemento en un vector

Tenga en cuenta algunos detalles aquí. Usamos el valor de índice de 2 para obtener el tercer elemento [1] porque los vectores se indexan por número, comenzando en cero. Usar & y [] nos da una referencia al elemento en el valor de índice. Cuando usamos el método get con el índice pasado como argumento [2], obtenemos un Option<&T> que podemos usar con match.

Rust proporciona estas dos maneras de referirse a un elemento para que puedas elegir cómo se comporta el programa cuando intentas usar un valor de índice fuera del rango de los elementos existentes. Como ejemplo, veamos qué pasa cuando tenemos un vector de cinco elementos y luego intentamos acceder a un elemento en el índice 100 con cada técnica, como se muestra en la Lista 8-5.

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];
let does_not_exist = v.get(100);

Lista 8-5: Intentar acceder al elemento en el índice 100 en un vector que contiene cinco elementos

Cuando ejecutamos este código, el primer método [] hará que el programa se detenga con un error porque hace referencia a un elemento que no existe. Este método es mejor usado cuando quieres que tu programa se detenga si hay un intento de acceder a un elemento más allá del final del vector.

Cuando se pasa un índice fuera del vector al método get, devuelve None sin detenerse con un error. Usarías este método si es posible que ocasionalmente se acceda a un elemento más allá del rango del vector en circunstancias normales. Tu código tendrá entonces la lógica para manejar tener ya sea Some(&element) o None, como se discutió en el Capítulo 6. Por ejemplo, el índice podría venir de una persona que ingresa un número. Si accidentalmente ingresan un número demasiado grande y el programa obtiene un valor None, podrías decirle al usuario cuántos elementos hay en el vector actual y darles otra oportunidad para ingresar un valor válido. Eso sería más amigable para el usuario que detener el programa debido a un error tipográfico.

Cuando el programa tiene una referencia válida, el verificador de préstamos (préstamo checker) fuerza las reglas de propiedad y préstamo (que se cubren en el Capítulo 4) para garantizar que esta referencia y cualquier otra referencia al contenido del vector sigan siendo válidas. Recuerda la regla que establece que no puedes tener referencias mutables e inmutables en el mismo ámbito. Esa regla se aplica en la Lista 8-6, donde mantenemos una referencia inmutable al primer elemento en un vector y tratamos de agregar un elemento al final. Este programa no funcionará si también intentamos referirnos a ese elemento más adelante en la función.

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {first}");

Lista 8-6: Intentar agregar un elemento a un vector mientras se tiene una referencia a un elemento

Compilar este código resultará en este error:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

El código en la Lista 8-6 puede parecer que debería funcionar: ¿por qué una referencia al primer elemento debería importarse por los cambios al final del vector? Este error se debe a la forma en que funcionan los vectores: dado que los vectores colocan los valores uno al lado del otro en la memoria, agregar un nuevo elemento al final del vector podría requerir asignar nueva memoria y copiar los elementos antiguos al nuevo espacio, si no hay suficiente espacio para poner todos los elementos uno al lado del otro donde se almacena actualmente el vector. En ese caso, la referencia al primer elemento estaría apuntando a memoria desasignada. Las reglas de préstamo evitan que los programas terminen en esa situación.

Nota: Para obtener más detalles sobre la implementación del tipo Vec<T>, consulte "The Rustonomicon" en https://doc.rust-lang.org/nomicon/vec/vec.html.

Iterar sobre los valores en un vector

Para acceder a cada elemento en un vector uno por uno, iteraríamos a través de todos los elementos en lugar de usar índices para acceder a uno a la vez. La Lista 8-7 muestra cómo usar un bucle for para obtener referencias inmutables a cada elemento en un vector de valores de tipo i32 y mostrarlos.

let v = vec![100, 32, 57];
for i in &v {
    println!("{i}");
}

Lista 8-7: Mostrar cada elemento en un vector iterando sobre los elementos usando un bucle for

También podemos iterar sobre referencias mutables a cada elemento en un vector mutable para poder hacer cambios a todos los elementos. El bucle for en la Lista 8-8 sumará 50 a cada elemento.

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Lista 8-8: Iterar sobre referencias mutables a elementos en un vector

Para cambiar el valor al que se refiere la referencia mutable, tenemos que usar el operador de desreferencia * para acceder al valor en i antes de poder usar el operador +=. Hablaremos más sobre el operador de desreferencia en "Siguiendo el puntero hacia el valor".

Iterar sobre un vector, ya sea inmutable o mutable, es seguro debido a las reglas del verificador de préstamos. Si intentáramos insertar o eliminar elementos en los cuerpos de los bucles for en las Listas 8-7 y 8-8, obtendríamos un error del compilador similar al que obtuvimos con el código en la Lista 8-6. La referencia al vector que mantiene el bucle for impide la modificación simultánea del vector completo.

Usar un enum para almacenar múltiples tipos

Los vectores solo pueden almacenar valores del mismo tipo. Esto puede resultar inconveniente; definitivamente hay casos de uso en los que es necesario almacenar una lista de elementos de diferentes tipos. Afortunadamente, las variantes de un enum se definen bajo el mismo tipo de enum, por lo que cuando necesitamos que un tipo represente elementos de diferentes tipos, ¡podemos definir y usar un enum!

Por ejemplo, digamos que queremos obtener valores de una fila en una hoja de cálculo en la que algunas de las columnas de la fila contienen enteros, algunos números de punto flotante y algunas cadenas. Podemos definir un enum cuyas variantes contendrán los diferentes tipos de valores, y todas las variantes del enum se considerarán del mismo tipo: el del enum. Luego podemos crear un vector para almacenar ese enum y, en última instancia, almacenar diferentes tipos. Lo hemos demostrado en la Lista 8-9.

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

Lista 8-9: Definir un enum para almacenar valores de diferentes tipos en un solo vector

Rust necesita saber qué tipos estarán en el vector en tiempo de compilación para saber exactamente cuánta memoria en el montón se necesitará para almacenar cada elemento. También debemos ser explícitos sobre qué tipos están permitidos en este vector. Si Rust permitiera que un vector almacenara cualquier tipo, habría una posibilidad de que uno o más de los tipos causaran errores con las operaciones realizadas en los elementos del vector. Usar un enum más una expresión match significa que Rust asegurará en tiempo de compilación que se maneje cada caso posible, como se discutió en el Capítulo 6.

Si no conoces el conjunto exhaustivo de tipos que un programa recibirá en tiempo de ejecución para almacenar en un vector, la técnica del enum no funcionará. En su lugar, puedes usar un objeto de tramo (trait object), que cubriremos en el Capítulo 17.

Ahora que hemos discutido algunas de las maneras más comunes de usar vectores, asegúrate de revisar la documentación de la API de todos los muchos métodos útiles definidos en Vec<T> por la biblioteca estándar. Por ejemplo, además de push, el método pop elimina y devuelve el último elemento.

Al eliminar un vector, se eliminan sus elementos

Como cualquier otro struct, un vector se libera cuando sale de ámbito, como se anota en la Lista 8-10.

{
    let v = vec![1, 2, 3, 4];

    // hacer cosas con v
} // <- v sale de ámbito y se libera aquí

Lista 8-10: Mostrando dónde se eliminan el vector y sus elementos

Cuando el vector se elimina, todos sus contenidos también se eliminan, lo que significa que los enteros que contiene se limpiarán. El verificador de préstamos asegura que cualquier referencia a los contenidos de un vector solo se use mientras el vector mismo es válido.

Pasemos a la siguiente tipo de colección: String!

Resumen

¡Felicitaciones! Has completado el laboratorio de Almacenar Listas de Valores con Vectores. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.