Las colecciones en la biblioteca estándar de Rust son herramientas esenciales para manejar datos cuyo tamaño no se conoce en tiempo de compilación. A diferencia de los arrays nativos, estas estructuras se almacenan en el heap (montículo), lo que permite que crezcan o disminuyan dinámicamente según las necesidades de la aplicación.
1. El vector dinámico: Vec<T>
El tipo Vec<T> es la colección más utilizada. Permite almacenar una secuencia contigua de elementos del mismo tipo con acceso indexado en tiempo constante O(1).
Estructura en memoria
Un vector consta de tres partes almacenadas en el stack: un puntero a los datos en el heap, la longitud actual (cuántos elementos contiene) y la capacidad (cuánto espacio total se ha reservado).
// Estructura interna simplificada
struct Vec<T> {
ptr: *mut T, // Dirección en el heap
len: usize, // Elementos actuales
cap: usize, // Espacio total disponible
}
Opreaciones comunes con Vectores
fn ejemplo_vectores() {
// Inicialización con capacidad predefinida para optimizar rendimiento
let mut edades: Vec<i32> = Vec::with_capacity(5);
// Uso de la macro vec!
let mut precios = vec![19.99, 25.50, 9.99];
// Inserción de elementos
precios.push(45.00);
precios.insert(1, 15.00); // Mueve los elementos siguientes (costoso: O(n))
// Acceso seguro mediante get
if let Some(precio) = precios.get(2) {
println!("El tercer precio es: {}", precio);
}
// Iteración mutable para aplicar descuentos
for p in precios.iter_mut() {
*p *= 0.90; // 10% de descuento
}
// Filtrado eficiente
precios.retain(|&p| p > 10.0);
}
2. Manejo de texto: String y &str
Rust separa las cadenas de texto en dos tipos principales para garantziar la seguridad de la memoria y la codificación UTF-8 válida.
- &str: Un "slice" de cadena, es una referencia inmutable a una secuencia de bytes UTF-8.
- String: Una cadena con propiedad (owned), modificable y con capacidad de crecimiento, que gestiona su propia memoria en el heap.
Manipulación de String
fn manejo_texto() {
// Creación desde un literal
let mut mensaje = String::from("Hola");
// Concatenación
mensaje.push_str(" Mundo");
mensaje.push('!');
// Uso de format! para construir cadenas complejas sin perder propiedad
let usuario = "Alex";
let log = format!("Acceso detectado: {}", usuario);
// Longitud: bytes vs caracteres
let texto_utf8 = "Rust 🦀";
println!("Bytes: {}", texto_utf8.len()); // 9 bytes (el emoji ocupa 4)
println!("Caracteres: {}", texto_utf8.chars().count()); // 6 caracteres
}
Errores frecuentes con cadenas
Intentar indexar una cadena como mensaje[0] fallará en Rust. Debido a que UTF-8 usa longitud variable, el primer "carácter" podría no ser el primer byte. Se debe usar el iterador chars() o manejar slices con precaución.
3. Mapas de Hash: HashMap<K, V>
HashMap almacena pares clave-valor. Es ideal para búsquedas rápidas donde se utiliza una clave única para recuperar un dato asociado.
Uso eficiente de HashMap
use std::collections::HashMap;
fn gestion_inventario() {
let mut stock = HashMap::new();
// Insertar datos
stock.insert(String::from("Laptop"), 10);
stock.insert(String::from("Monitor"), 25);
// Actualización inteligente con Entry API
// Si la clave no existe, inserta 0 y luego suma 1
let cantidad_ratones = stock.entry(String::from("Mouse")).or_insert(0);
*cantidad_ratones += 5;
// Búsqueda segura
match stock.get("Laptop") {
Some(unidades) => println!("Unidades disponibles: {}", unidades),
None => println!("Producto no registrado"),
}
}
Comparativa de errores comunes
| Error Común | Consecuencia | Solución Recomendada |
|---|---|---|
| Acceso por índice fuera de límites | Panic (caída del programa) | Usar el método .get() que devuelve Option |
| Modificar un Vec mientras se itera | Error de compilación (Borrow checker) | Coleccionar cambios primero o usar retain |
| Confundir bytes con caracteres en String | Lógica incorrecta o pánico | Usar .chars() para lógica de texto |
| Re-hashear constantemente en HashMap | Bajo rendimiento | Usar with_capacity si se conoce el volumen de datos |
Implementación Práctica: Sistema de Inventario Escolar
A continuación, se presenta un ejemplo que integra las tres estructuras para gestionar una base de datos de estudiantes y sus calificaciones.
use std::collections::HashMap;
struct Alumno {
nombre: String,
notas: Vec<f32>,
}
struct RegistroEscolar {
// Usamos el ID como clave para acceso O(1)
estudiantes: HashMap<u32, Alumno>,
}
impl RegistroEscolar {
fn nuevo() -> Self {
Self { estudiantes: HashMap::new() }
}
fn inscribir(&mut self, id: u32, nombre: &str) {
let nuevo_alumno = Alumno {
nombre: nombre.to_string(),
notas: Vec::new(),
};
self.estudiantes.entry(id).or_insert(nuevo_alumno);
}
fn agregar_nota(&mut self, id: u32, nota: f32) -> Result<(), String> {
let alumno = self.estudiantes.get_mut(&id)
.ok_or_else(|| format!("ID {} no encontrado", id))?;
alumno.notas.push(nota);
Ok(())
}
fn promedio_general(&self) {
for (id, alumno) in &self.estudiantes {
let suma: f32 = alumno.notas.iter().sum();
let count = alumno.notas.len();
let promedio = if count > 0 { suma / count as f32 } else { 0.0 };
println!("ID: {} | Estudiante: {} | Promedio: {:.2}",
id, alumno.nombre, promedio);
}
}
}
fn main() {
let mut sistema = RegistroEscolar::nuevo();
sistema.inscribir(101, "Sofia");
sistema.inscribir(102, "Mateo");
let _ = sistema.agregar_nota(101, 9.5);
let _ = sistema.agregar_nota(101, 8.0);
let _ = sistema.agregar_nota(102, 7.0);
sistema.promedio_general();
}
Este sistema demuestra cómo el HashMap facilita la búsqueda por identificador único, el Vec permite almacenar una lista variable de notas y el String gestiona los nombres de forma segura.