En esta página

Colecciones: Vec, String y HashMap

12 min lectura TextoCap. 3 — Tipos compuestos

Colecciones en Rust

Rust proporciona varias colecciones en su biblioteca estándar, todas ubicadas en std::collections. Las tres más importantes son Vec<T>, String y HashMap<K, V>.

Vec: el array dinámico

Vec<T> es la colección más usada en Rust. Es un array de tamaño dinámico que almacena elementos del mismo tipo en el heap:

fn main() {
    // Crear un Vec vacío — necesitas especificar el tipo
    let mut v1: Vec<i32> = Vec::new();
    
    // Con la macro vec!
    let v2 = vec![1, 2, 3, 4, 5];
    
    // Con capacidad inicial (evita realocaciones)
    let mut v3: Vec<String> = Vec::with_capacity(10);
    
    // Agregar elementos
    v1.push(10);
    v1.push(20);
    v1.push(30);
    
    // Eliminar y retornar el último
    let ultimo = v1.pop(); // Some(30)
    println!("{:?}", ultimo);
    
    // Insertar en una posición específica
    v1.insert(0, 5); // Inserta 5 al inicio
    
    // Eliminar por índice
    let eliminado = v1.remove(0); // Elimina y retorna el elemento en índice 0
    println!("Eliminado: {eliminado}");
    
    println!("v1: {:?}", v1);
    println!("v2: {:?}", v2);
}

Acceso a elementos

fn main() {
    let v = vec![10, 20, 30, 40, 50];
    
    // Acceso por índice — puede entrar en pánico si está fuera de rango
    let tercero = v[2];
    println!("Tercero: {tercero}");
    
    // Acceso seguro con get() — retorna Option<&T>
    match v.get(100) {
        Some(val) => println!("Valor: {val}"),
        None      => println!("Índice fuera de rango"),
    }
    
    // Slices de Vec
    let porcion: &[i32] = &v[1..4]; // [20, 30, 40]
    println!("Porción: {:?}", porcion);
    
    // first() y last()
    println!("Primero: {:?}", v.first());
    println!("Último: {:?}", v.last());
}

Iteración sobre Vec

fn main() {
    let mut numeros = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // Iterar con referencia (no consume el Vec)
    for n in &numeros {
        print!("{n} ");
    }
    println!();
    
    // Iterar con referencia mutable
    for n in &mut numeros {
        *n *= 2;
    }
    println!("{:?}", numeros);
    
    // Iterar por valor (consume el Vec)
    let cuadrados: Vec<i32> = numeros.into_iter().map(|n| n * n).collect();
    println!("{:?}", cuadrados);
    
    // Filtrar
    let pares: Vec<i32> = cuadrados.iter().filter(|&&n| n % 2 == 0).cloned().collect();
    println!("Pares: {:?}", pares);
    
    // enumerate
    for (i, val) in pares.iter().enumerate() {
        println!("[{i}] = {val}");
    }
}

Operaciones comunes de Vec

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
    
    println!("Longitud: {}", v.len());
    println!("¿Vacío? {}", v.is_empty());
    println!("¿Contiene 5? {}", v.contains(&5));
    
    // Ordenar
    v.sort();
    println!("Ordenado: {:?}", v);
    
    // Deduplicar (requiere ordenado previo)
    v.dedup();
    println!("Sin duplicados: {:?}", v);
    
    // Invertir
    v.reverse();
    println!("Invertido: {:?}", v);
    
    // Truncar
    v.truncate(5);
    println!("Truncado a 5: {:?}", v);
    
    // retain: conservar solo los que cumplen la condición
    v.retain(|&n| n > 2);
    println!("Solo > 2: {:?}", v);
    
    // extend: agregar elementos de otro iterable
    v.extend([10, 20, 30]);
    println!("Extendido: {:?}", v);
    
    // split_at
    let (izquierda, derecha) = v.split_at(3);
    println!("Izquierda: {:?}, Derecha: {:?}", izquierda, derecha);
}

String: texto UTF-8 poseído

String es un vector de bytes UTF-8 con ownership. Diferente de &str, un String puede crecer y modificarse:

fn main() {
    // Crear
    let mut s = String::new();
    let s2 = String::from("Hola");
    let s3 = "mundo".to_string();
    
    // Agregar texto
    s.push_str("¡Bienvenido a Rust!");
    s.push('!'); // Un solo char
    
    // Concatenación con +
    // Nota: s2 se mueve (el operador + toma ownership del primer argumento)
    let s4 = s2 + " " + &s3;
    println!("{s4}");
    
    // format!: concatenar sin mover
    let parte1 = String::from("a");
    let parte2 = String::from("b");
    let parte3 = String::from("c");
    let unida = format!("{parte1}-{parte2}-{parte3}"); // Ninguna se mueve
    println!("{unida}");
    
    // Métodos útiles
    let texto = String::from("  Hola, Rust!  ");
    println!("Trim: '{}'", texto.trim());
    println!("Mayúsculas: {}", texto.to_uppercase());
    println!("Minúsculas: {}", texto.to_lowercase());
    println!("Reemplazar: {}", texto.replace("Rust", "Mundo"));
    
    // Dividir
    let csv = "uno,dos,tres,cuatro";
    let partes: Vec<&str> = csv.split(',').collect();
    println!("{:?}", partes);
    
    // Verificar
    println!("¿Contiene 'Rust'? {}", texto.contains("Rust"));
    println!("¿Empieza con '  Hola'? {}", texto.starts_with("  Hola"));
    
    // Longitud — en bytes, no en caracteres
    let emoji = "🦀";
    println!("Bytes de '{}': {}", emoji, emoji.len()); // 4
    println!("Chars de '{}': {}", emoji, emoji.chars().count()); // 1
}

HashMap: el mapa clave-valor

HashMap<K, V> asocia claves de tipo K con valores de tipo V. Las claves deben implementar Eq y Hash:

use std::collections::HashMap;

fn main() {
    let mut poblacion: HashMap<&str, u64> = HashMap::new();
    
    poblacion.insert("Argentina", 46_000_000);
    poblacion.insert("Bolivia", 12_000_000);
    poblacion.insert("Chile", 19_500_000);
    poblacion.insert("Colombia", 51_000_000);
    
    // Buscar
    println!("{:?}", poblacion.get("Bolivia")); // Some(12000000)
    println!("{}", poblacion["Argentina"]);     // 46000000 (puede entrar en pánico)
    
    // contains_key
    println!("¿Tiene Peru? {}", poblacion.contains_key("Peru"));
    
    // Eliminar
    let removido = poblacion.remove("Chile");
    println!("Removido: {:?}", removido);
    
    // Iterar (orden no garantizado)
    let mut paises: Vec<&&str> = poblacion.keys().collect();
    paises.sort();
    for pais in paises {
        println!("{}: {}", pais, poblacion[pais]);
    }
    
    // Longitud
    println!("Total países: {}", poblacion.len());
}

La entry API

La API entry es la forma idiomática de trabajar con valores que pueden o no existir:

use std::collections::HashMap;

fn contar_palabras(texto: &str) -> HashMap<&str, usize> {
    let mut conteo = HashMap::new();
    
    for palabra in texto.split_whitespace() {
        // Si la clave no existe, inserta 0; luego incrementa
        let count = conteo.entry(palabra).or_insert(0);
        *count += 1;
    }
    
    conteo
}

fn main() {
    let texto = "el gato come el ratón el gato duerme";
    let conteo = contar_palabras(texto);
    
    // Ordenar para output determinístico
    let mut palabras: Vec<(&&str, &usize)> = conteo.iter().collect();
    palabras.sort_by_key(|&(p, _)| *p);
    
    for (palabra, n) in palabras {
        println!("{palabra}: {n}");
    }
    
    // or_insert_with: calcular el valor por defecto solo si es necesario
    let mut cache: HashMap<String, Vec<i32>> = HashMap::new();
    cache.entry(String::from("clave")).or_insert_with(Vec::new).push(42);
    cache.entry(String::from("clave")).or_insert_with(Vec::new).push(99);
    println!("{:?}", cache);
}

Crear HashMap desde iteradores

use std::collections::HashMap;

fn main() {
    // Desde dos vectores paralelos
    let claves = vec!["uno", "dos", "tres"];
    let valores = vec![1, 2, 3];
    
    let mapa: HashMap<&str, i32> = claves.into_iter().zip(valores).collect();
    println!("{:?}", mapa);
    
    // Desde un Vec de tuplas
    let pares = vec![("a", 1), ("b", 2), ("c", 3)];
    let mapa2: HashMap<&str, i32> = pares.into_iter().collect();
    println!("{:?}", mapa2);
}

Con el dominio de las colecciones principales, estás listo para explorar los traits — el sistema de abstracciones de Rust que permite el polimorfismo sin herencia.

collect() con turbofish
Cuando Rust no puede inferir el tipo de collect(), usa turbofish: iterator.collect::<Vec<_>>() o anota el tipo de la variable. El _ dentro de Vec<_> le dice a Rust que infiera el tipo del elemento.
entry API para conteo
El patrón scores.entry(clave).or_insert(0) es idiomático para conteo de frecuencias. El método or_insert devuelve una referencia mutable al valor (nuevo o existente), que puedes incrementar: *conteo += 1;
rust
use std::collections::HashMap;

fn main() {
    // === Vec<T> ===
    let mut numeros: Vec<i32> = Vec::new();
    numeros.push(1);
    numeros.push(2);
    numeros.push(3);

    // Macro vec! para inicializar
    let frutas = vec!["manzana", "banana", "cereza"];

    // Iteración
    let dobles: Vec<i32> = numeros.iter().map(|n| n * 2).collect();
    println!("{:?}", dobles);

    // === HashMap ===
    let mut scores: HashMap<String, u32> = HashMap::new();
    scores.insert(String::from("Ana"), 95);
    scores.insert(String::from("Bob"), 87);

    // entry API: insertar solo si no existe
    scores.entry(String::from("Ana")).or_insert(100);
    scores.entry(String::from("Carlos")).or_insert(78);

    for (nombre, score) in &scores {
        println!("{nombre}: {score}");
    }

    // Buscar
    if let Some(score) = scores.get("Ana") {
        println!("Score de Ana: {score}");
    }
}