Colecciones: Vec, String, HashMap
Vec, String y HashMap son las tres colecciones que usarás constantemente, y cada una esconde una trampa que vale la pena entender a tiempo.
Ya conoces los arrays de tamaño fijo, cuya longitud queda grabada en su tipo y se conoce en tiempo de compilación — útiles, pero poco adecuados para 'no sé cuántos de estos voy a tener hasta que el programa se ejecute.' Las colecciones redimensionables de la biblioteca estándar llenan ese hueco, y tres de ellas cubren la inmensa mayoría del Rust cotidiano: Vec<T> para una lista ordenada y redimensionable, String para texto UTF-8 propio y creciente, y HashMap<K, V> para búsquedas de valor por clave. Las tres viven en el heap, las tres pueden crecer y encogerse en tiempo de ejecución, y las tres te siguen dando las garantías de propiedad y préstamo que ya aprendiste — 'dinámico' no significa en ningún momento 'sin verificar.'
Un Vec<T> empieza vacío con Vec::new() (o ya lleno con la macro vec![...]) y crece con .push(valor), que añade al final, y se reduce con .pop(), que elimina y devuelve el último elemento como un Option<T> — Some(valor) si había uno, None si el vector ya estaba vacío. Puedes leer un elemento por posición con corchetes, numbers[0], e iterar todo el vector con un bucle for sobre &numbers, que toma prestado cada elemento en lugar de apropiarse del vector. Reserva la iteración por propiedad (for n in numbers) para cuando ya hayas terminado con el Vec después y realmente quieras consumirlo elemento por elemento.
Ese indexado con corchetes es cómodo, pero entra en pánico — de forma inmediata e incondicional — si el índice está fuera de rango, porque Rust prefiere fallar ruidosamente antes que leer memoria basura en silencio o dar la vuelta como hacen algunos lenguajes. Cuando el índice no está garantizado como válido — entrada del usuario, un desplazamiento calculado, cualquier cosa que no hayas verificado ya — recurre en su lugar a .get(indice), que devuelve Option<&T>: Some(&valor) para un índice válido, None para uno fuera de rango, dejándote manejar el fallo en lugar de estrellarte por él. La regla práctica: usa [] cuando estar fuera de rango sería en sí mismo un error que merece hacer fallar el programa, y .get() siempre que la validez del índice esté genuinamente en duda.
Rust tiene dos tipos de cadena que cumplen dos funciones distintas, y la división coincide exactamente con la propiedad. String es propio y redimensionable — administra su propio buffer en el heap, se puede construir con push_str o +, y es al que recurres cuando necesitas poseer texto o construirlo poco a poco. &str es una vista prestada sobre texto UTF-8 que vive en otro lugar — un literal de cadena grabado en el binario ("hello", en la práctica un &'static str), o un slice de un String existente — y es lo que las funciones deberían aceptar como parámetro siempre que solo necesiten leer texto, ya que un parámetro &str acepta con gusto tanto un literal como un String prestado sin forzar ninguna asignación en quien llama.
Ambos tipos almacenan sus bytes como UTF-8 válido por debajo, y UTF-8 es una codificación de ancho variable: los caracteres ASCII como 'a' ocupan un byte, mientras que caracteres como 'é' o los emoji pueden ocupar dos, tres o cuatro. Por eso, precisamente, ni String ni &str admiten my_string[0] como 'dame el primer carácter' — un índice numérico sería un desplazamiento en bytes, y no hay ninguna garantía de que ese desplazamiento caiga en un límite de carácter en vez de partir por la mitad uno de varios bytes, lo cual produciría un fragmento de bytes que ni siquiera sería UTF-8 válido. En su lugar, tienes que ser explícito sobre qué unidad quieres decir: .len() te da la longitud en bytes, .chars().count() te da el número de caracteres (y pueden diferir de verdad, como en "café", que son 5 bytes pero 4 caracteres), y .chars().next() te da el primer carácter de forma segura, un carácter completo a la vez sin importar cuántos bytes ocupe.
HashMap<K, V> completa el trío para cuando la posición no importa pero sí la búsqueda por clave. .insert(clave, valor) añade o sobrescribe una entrada, y .get(&clave) devuelve Option<&V> — Some si la clave existe, None si no — siguiendo el mismo patrón de 'nada de nulos silenciosos' que ya has visto por todo este curso. La entry API es la forma idiomática de manejar 'insertar un valor por defecto si falta, y luego actualizar' en un solo paso sin buscar la clave dos veces: map.entry(clave).or_insert(0) devuelve una referencia mutable al valor existente, o inserta primero 0 y devuelve una referencia a eso, que luego puedes incrementar en el lugar — un patrón que usarás constantemente para contar, agrupar y acumular.
Fíjate en que las tres colecciones son genéricas — Vec<T>, HashMap<K, V> — que es lo que permite que una sola implementación de Vec sirva por igual a Vec<i32>, Vec<String> y Vec<Rectangle> sin duplicar ni una línea de código. Has estado usando genéricos de manera informal durante toda esta lección sin nombrarlos; la siguiente lección hace explícito ese mecanismo, y lo combina con los traits, que describen qué puede hacer un tipo en lugar de qué datos contiene — juntos, son la forma en que Rust escribe una sola función o una sola colección que funciona correctamente para muchos tipos, sin renunciar a nada de la verificación en tiempo de compilación en la que has confiado hasta ahora.
fn main() {let mut numbers: Vec<i32> = Vec::new();numbers.push(10);numbers.push(20);numbers.push(30);println!("first = {}", numbers[0]); // indexing panics if the index is out of boundsif let Some(last) = numbers.pop() {println!("popped {}", last);}for n in &numbers {println!("n = {}", n);}// get() returns Option instead of panicking on an out-of-range index.match numbers.get(10) {Some(value) => println!("value at 10: {}", value),None => println!("index 10 is out of bounds"),}}
Building a Vec with push/pop, iterating it by reference, and contrasting panicking [] indexing with the Option-returning get() for an out-of-range index.
fn main() {let owned: String = String::from("hola, "); // owned, growablelet borrowed: &str = "mundo!"; // borrowed string slice, often 'staticlet mut greeting = owned;greeting.push_str(borrowed);println!("{}", greeting);let word = "café"; // 'c', 'a', 'f' are 1 byte each in UTF-8; 'é' takes 2 bytesprintln!("byte length: {}", word.len());println!("char count: {}", word.chars().count());// word[0] would not compile: String/&str do not support direct integer indexing,// precisely because a byte offset can land in the middle of a multi-byte character.if let Some(first_char) = word.chars().next() {println!("first char: {}", first_char);}}
String (owned, growable) versus &str (borrowed), and why UTF-8's variable-width encoding makes "café".len() (bytes) differ from its character count.
use std::collections::HashMap;fn main() {let mut scores: HashMap<String, i32> = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Yellow"), 50);match scores.get("Blue") {Some(score) => println!("Blue: {}", score),None => println!("Blue: no score yet"),}// entry API: insert a default only if the key is missing, then update it in place.let team = String::from("Blue");let count = scores.entry(team).or_insert(0);*count += 1;for (key, value) in &scores {println!("{}: {}", key, value);}}
HashMap insert and get return Option the same way Vec's get() does, and the entry API inserts a default only when a key is missing before updating it in place.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Why does `my_string[0]` fail to compile for a Rust String, even though `my_vec[0]` compiles fine for a Vec<T>?