8. Colecciones e iteradores
Ejemplo completo: examples/l08_collections_iterators.rs — cargo run --example l08_collections_iterators.
Las colecciones que ya conoces
Sección titulada «Las colecciones que ya conoces»Rust (std::collections) |
C# | Java |
|---|---|---|
Vec<T> |
List<T> |
ArrayList<T> |
VecDeque<T> |
Queue<T> / LinkedList<T> |
ArrayDeque<T> |
HashMap<K, V> |
Dictionary<K, V> |
HashMap<K, V> |
BTreeMap<K, V> |
SortedDictionary<K, V> |
TreeMap<K, V> |
HashSet<T> |
HashSet<T> |
HashSet<T> |
BTreeSet<T> |
SortedSet<T> |
TreeSet<T> |
BinaryHeap<T> |
PriorityQueue<T, P> |
PriorityQueue<T> |
Vec y String están disponibles en todas partes; las demás necesitan un use std::collections::….
LINQ y Streams, traducidos
Sección titulada «LINQ y Streams, traducidos»El ejemplo trabaja con una lista de pedidos:
let big_orders: Vec<&str> = orders .iter() .filter(|o| o.quantity >= 2) .map(|o| o.product) .collect();// big orders: ["mouse", "monitor"]| Rust | LINQ de C# | Streams de Java |
|---|---|---|
.iter() |
(el propio IEnumerable) |
.stream() |
.filter(|x| …) |
.Where(x => …) |
.filter(x -> …) |
.map(|x| …) |
.Select(x => …) |
.map(x -> …) |
.flat_map(|x| …) |
.SelectMany(x => …) |
.flatMap(x -> …) |
.collect::<Vec<_>>() |
.ToList() |
.toList() |
.sum() |
.Sum() |
.mapToInt(…).sum() |
.count() |
.Count() |
.count() |
.any(…) / .all(…) |
.Any(…) / .All(…) |
.anyMatch(…) / .allMatch(…) |
.find(…) |
.FirstOrDefault(…) |
.filter(…).findFirst() |
.fold(init, |acc, x| …) |
.Aggregate(init, …) |
.reduce(init, …) |
.take(n) / .skip(n) |
.Take(n) / .Skip(n) |
.limit(n) / .skip(n) |
.zip(other) |
.Zip(other) |
— |
.enumerate() |
.Select((x, i) => …) |
— |
.min_by_key(…) / .max_by_key(…) |
.MinBy(…) / .MaxBy(…) |
.min(comparator) |
recoger en un HashSet |
.Distinct() |
.distinct() |
vec.sort_by_key(…) (sobre el Vec) |
.OrderBy(…) |
.sorted(comparator) |
let revenue: f64 = orders.iter().map(|o| o.quantity as f64 * o.unit_price).sum();let any_monitor = orders.iter().any(|o| o.product == "monitor");let all_positive = orders.iter().all(|o| o.quantity > 0);let first_mouse = orders.iter().find(|o| o.product == "mouse").map(|o| o.customer);revenue 562, any monitor true, all positive true, first mouse buyer Some("grace")find devuelve un Option — no existe un FirstOrDefault que devuelva null.
Los iteradores son perezosos
Sección titulada «Los iteradores son perezosos»Como la ejecución diferida de LINQ y las operaciones intermedias de Java, nada se ejecuta hasta que algo consume el iterador (collect, sum, for, count…). El compilador te avisa si lo olvidas:
numbers.iter().map(|n| println!("{n}"));warning: unused `Map` that must be used --> e08_lazy.rs:3:5 |3 | numbers.iter().map(|n| println!("{n}")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: iterators are lazy and do nothing unless consumed…help: you might have meant to use `Iterator::for_each`Esta pereza también permite que los iteradores sean infinitos: consulta el ejemplo de Fibonacci más abajo.
collect necesita conocer el tipo de destino
Sección titulada «collect necesita conocer el tipo de destino»collect puede construir un Vec, un HashSet, un String, un HashMap… así que tienes que decir cuál:
let evens = (1..10).filter(|n| n % 2 == 0).collect();error[E0283]: type annotations needed --> e08_collect_type.rs:2:9 | 2 | let evens = (1..10).filter(|n| n % 2 == 0).collect(); | ^^^^^ ------- type must be known at this point | = note: cannot satisfy `_: FromIterator<i32>`…help: consider giving `evens` an explicit type | 2 | let evens: Vec<_> = (1..10).filter(|n| n % 2 == 0).collect(); | ++++++++Anota la variable (let evens: Vec<_> = …) o usa la sintaxis «turbofish»: .collect::<Vec<_>>(). El _ deja que el compilador infiera el tipo de los elementos.
iter, iter_mut, into_iter
Sección titulada «iter, iter_mut, into_iter»La propiedad (lecciones 3 y 4) se nota en la forma de iterar:
| Método | Produce | La colección después |
|---|---|---|
v.iter() o for x in &v |
&T |
sin cambios, sigue siendo utilizable |
v.iter_mut() o for x in &mut v |
&mut T |
modificada en el sitio |
v.into_iter() o for x in v |
T |
movida, ya no es utilizable |
let mut prices = vec![10.0, 20.0, 30.0];for p in prices.iter_mut() { *p *= 1.2; // `*` escribe a través de la referencia}let total: f64 = prices.into_iter().sum(); // consume pricesLa trampa clásica es un bucle for sobre la propia colección:
error[E0382]: borrow of moved value: `prices` --> e08_into_iter_move.rs:6:16 | 2 | let prices = vec![10.0, 20.0]; | ------ move occurs because `prices` has type `Vec<f64>`, which does not implement the `Copy` trait 3 | for p in prices { | ------ `prices` moved due to this implicit call to `.into_iter()`... 6 | println!("{prices:?}"); | ^^^^^^ value borrowed here after move…help: consider iterating over a slice of the `Vec<f64>`'s content to avoid moving into the `for` loop | 3 | for p in &prices { | +Agrupar con HashMap::entry
Sección titulada «Agrupar con HashMap::entry»No hay GroupBy en la biblioteca estándar; la API entry lo resuelve en una línea — como CollectionsMarshal.GetValueRefOrAddDefault en C# o merge en Java:
let mut spend: HashMap<&str, f64> = HashMap::new();for o in &orders { *spend.entry(o.customer).or_insert(0.0) += o.quantity as f64 * o.unit_price;}let sorted: BTreeMap<_, _> = spend.iter().collect();// spend per customer: {"ada": 487.0, "grace": 50.0, "linus": 25.0}Ordenar, y por qué los flotantes son especiales
Sección titulada «Ordenar, y por qué los flotantes son especiales»sort necesita un orden total (Ord). Los números de coma flotante solo tienen uno parcial, porque NaN no se puede comparar con nada:
let mut prices = vec![19.99, 5.0, 12.5];prices.sort();error[E0277]: the trait bound `{float}: Ord` is not satisfied --> e08_sort_floats.rs:3:12 | 3 | prices.sort(); | ^^^^ the trait `Ord` is not implemented for `{float}`C# y Java ordenan los doubles sin rechistar y colocan NaN según su propia convención. En Rust, eliges explícitamente:
let mut prices: Vec<f64> = vec![19.99, 5.0, 12.5];prices.sort_by(|a, b| a.total_cmp(b)); // [5.0, 12.5, 19.99]Closures
Sección titulada «Closures»Las closures de Rust (|args| body) son las lambdas de C# y de Java. Capturan variables del ámbito que las rodea — por referencia, por defecto:
let threshold = 50.0;let is_expensive = |o: &Order| o.unit_price * o.quantity as f64 > threshold;println!("expensive orders: {}", orders.iter().filter(|o| is_expensive(o)).count()); // 2move hace que la closure tome la propiedad de lo que captura — obligatorio cuando la closure vive más que el ámbito actual, por ejemplo en un hilo (lección 12):
let label = String::from("report");let make_title = move |n: usize| format!("{label} #{n}");println!("{}", make_title(1)); // report #1| C# | Java | Rust | |
|---|---|---|---|
| Capturas | variables (elevadas a una clase de closure) | variables efectivamente finales | por referencia, por referencia mutable o por valor (move) |
| Tipos de función | Func<>, Action<> |
Function, Consumer, … |
los traits Fn, FnMut, FnOnce |
Escribir tu propio iterador
Sección titulada «Escribir tu propio iterador»Implementa un solo método, next, y todos los adaptadores anteriores quedan disponibles — el equivalente de IEnumerable<T> con yield return:
struct Fibonacci { current: u64, next: u64,}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<Self::Item> { let value = self.current; self.current = self.next; self.next += value; Some(value) // nunca None: una secuencia infinita }}
let fibs: Vec<u64> = Fibonacci { current: 0, next: 1 }.take_while(|&n| n < 100).collect();// fibonacci < 100: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]type Item = u64; es un tipo asociado: cada iterador decide qué produce.
Extra sobre slices: windows y chunks
Sección titulada «Extra sobre slices: windows y chunks»let readings = [3, 5, 4, 8, 9];let rising = readings.windows(2).filter(|w| w[1] > w[0]).count(); // 3let batches: Vec<i32> = readings.chunks(2).map(|c| c.iter().sum()).collect(); // [8, 12, 9]Puntos clave
Sección titulada «Puntos clave»Vec,HashMap,HashSetyBTreeMapcorresponden directamente a las colecciones que ya conoces; el orden de unHashMapno está especificado.- Los adaptadores de iteradores son LINQ/Streams: perezosos hasta que se consumen, y
collectnecesita un tipo de destino. iter,iter_muteinto_itertoman prestada la colección, la toman prestada de forma mutable o la consumen.- Las closures capturan por referencia salvo que escribas
move; implementarnextte da un iterador completo.
Ejercicios
Sección titulada «Ejercicios»- Traduce esta consulta LINQ a una cadena de iteradores:
var result = words.Where(w => w.Length > 3) .Select(w => w.ToUpper()) .OrderBy(w => w) .ToList();Solución
let words = ["tree", "sky", "apple", "rust", "go"];let mut result: Vec<String> = words .iter() .filter(|w| w.len() > 3) .map(|w| w.to_uppercase()) .collect();result.sort();assert_eq!(result, ["APPLE", "RUST", "TREE"]);Ordenar es un método de Vec (ordena en el sitio), no un adaptador de iterador, así que primero hay que recoger. len() cuenta bytes; para palabras no ASCII, usa w.chars().count().
- Escribe
fn word_counts(text: &str) -> BTreeMap<String, usize>que cuente las palabras sin distinguir mayúsculas de minúsculas, de modo que"the cat and THE hat"dé{"and": 1, "cat": 1, "hat": 1, "the": 2}.
Solución
use std::collections::BTreeMap;
fn word_counts(text: &str) -> BTreeMap<String, usize> { let mut counts = BTreeMap::new(); for word in text.split_whitespace() { *counts.entry(word.to_lowercase()).or_insert(0) += 1; } counts}
let counts = word_counts("the cat and THE hat");assert_eq!(counts["the"], 2);assert_eq!(counts.keys().collect::<Vec<_>>(), ["and", "cat", "hat", "the"]);BTreeMap mantiene las claves ordenadas, así que la salida es determinista.
- Implementa un iterador
Countdown(u32)que produzca3, 2, 1paraCountdown(3)y luego se detenga, y úsalo conmapycollectpara construir"3... 2... 1...".
Solución
struct Countdown(u32);
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.0 == 0 { None } else { self.0 -= 1; Some(self.0 + 1) } }}
let text: Vec<String> = Countdown(3).map(|n| format!("{n}...")).collect();assert_eq!(text.join(" "), "3... 2... 1...");Devolver None termina la iteración, como yield break o el final de un IEnumerable.