Trucos de la biblioteca estándar de Rust: herramientas para cada día
En toda cocina hay un cajón de herramientas pequeñas: un pelador, un abrebotellas, un prensaajos. Podrías hacerlo todo con un cuchillo grande, pero las herramientas pequeñas hacen el trabajo más rápido y con menos riesgo. La biblioteca estándar de Rust también tiene un cajón así.
Esta página es un recorrido por ese cajón. Cada truco es corto: una imagen de la vida real, un ejemplo que puedes ejecutar y una línea “úsalo cuando…” para que sepas cuándo echar mano de él. No hace falta que los aprendas todos de golpe. Dale un vistazo ahora y vuelve cuando veas en tu propio código algo que te resulte torpe.
Sacar un valor de un &mut: mem::take, replace, swap
Sección titulada «Sacar un valor de un &mut: mem::take, replace, swap»Imagina un cubo de basura con una bolsa dentro. Puedes sacar la bolsa llena, pero solo si pones una bolsa nueva enseguida. El cubo nunca se queda sin bolsa.
Rust tiene la misma regla. Si solo tienes &mut self, no puedes mover un campo hacia fuera sin más, porque dejarías un hueco en el struct. El compilador te lo impide con E0507. Las funciones de std::mem lo resuelven dejando siempre algo en su lugar:
mem::take(&mut x)se lleva el valor y deja el valor por defecto del tipo (unVecvacío, unStringvacío,0,None).mem::replace(&mut x, new)ponenewen su lugar y te devuelve el valor antiguo.Option::take()se lleva lo que hay dentro de unOptiony dejaNone.
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // Entrega todos los mensajes sin leer y deja un Vec vacío en su lugar. fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// Pone un estado nuevo y devuelve el anterior. fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// Saca el borrador (si lo hay) y deja None. fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["¿Almorzamos?".to_string(), "Factura #42".to_string()], status: "conectado".to_string(), draft: Some("Nos vemos a las 6".to_string()), };
let messages = inbox.read_all(); println!("leídos {:?}, sin leer todavía: {:?}", messages, inbox.unread);
let old = inbox.set_status("en una reunión"); println!("el estado era '{old}', ahora es '{}'", inbox.status);
println!("enviado: {:?}", inbox.send_draft()); println!("enviado otra vez: {:?}", inbox.send_draft());}Salida:
leídos ["¿Almorzamos?", "Factura #42"], sin leer todavía: []el estado era 'conectado', ahora es 'en una reunión'enviado: Some("Nos vemos a las 6")enviado otra vez: NoneNo hay ni un clone(): los mensajes y el estado antiguo se mueven, no se copian. Por eso estas funciones son seguras y además baratas.
Dos parientes completan el juego. mem::swap(&mut a, &mut b) intercambia dos valores, como cambiar de sitio dos platos en la mesa. Option::replace(new) mete un valor nuevo en un Option y te devuelve el antiguo.
use std::mem;
fn main() { // Llena una lista mientras se trabaja con la otra. let mut filling = vec!["pedido 1", "pedido 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("procesando {processing:?}, llenando {filling:?}");
// Option::replace: mete un valor nuevo y te devuelve el antiguo. let mut now_playing = Some("Canción A"); let previous = now_playing.replace("Canción B"); println!("antes {previous:?}, ahora {now_playing:?}");}Salida:
procesando ["pedido 1", "pedido 2"], llenando []antes Some("Canción A"), ahora Some("Canción B")Úsalo cuando solo tienes acceso &mut pero necesitas quedarte con el valor que hay dentro.
Slices: windows, chunks y compañía
Sección titulada «Slices: windows, chunks y compañía»Piensa en windows(2) como una lupa que se desliza por una regla: te enseña dos marcas a la vez y luego avanza un paso. chunks(3) es como meter huevos en cajas de tres: la última caja puede quedar a medias. chunks_exact(3) solo te da cajas llenas y guarda los huevos sobrantes aparte, en .remainder().
Estos métodos funcionan con cualquier slice (porción), así que también sirven para arrays y vectores.
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2): cada pareja de vecinos, solapadas for pair in temps.windows(2) { println!("{} -> {}: cambio {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3): lotes de 3, el último puede ser más pequeño let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("enviando a {batch:?}"); }
// chunks_exact(2): solo lotes completos; el resto espera en remainder() let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("pareja {pair:?}"); } println!("sobrante: {leftover:?}");}Salida:
12 -> 15: cambio 315 -> 14: cambio -114 -> 19: cambio 519 -> 21: cambio 2enviando a ["[email protected]", "[email protected]", "[email protected]"]enviando a ["[email protected]", "[email protected]"]pareja [1, 2]pareja [3, 4]sobrante: [5]Algunos ayudantes más de los slices responden preguntas habituales sin necesidad de un bucle. split_first() te da el primer elemento y el resto, como una maestra que lee el primer nombre de la lista y le pasa el resto a un ayudante. split_last() hace lo mismo desde el final.
fn main() { let command = ["git", "commit", "-m", "corrige una errata"];
// split_first: el primer elemento y un slice con el resto if let Some((program, args)) = command.split_first() { println!("programa: {program}, argumentos: {args:?}"); }
// split_last: el último elemento y todo lo que va antes if let Some((message, _rest)) = command.split_last() { println!("mensaje: {message}"); }
println!("¿es git? {}", command.starts_with(&["git"])); println!("¿tiene -m? {}", command.contains(&"-m"));
// iter().rev(): recorre al revés sin cambiar el slice let history = ["abrir", "editar", "guardar"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("orden para deshacer: {undo_order:?}");}Salida:
programa: git, argumentos: ["commit", "-m", "corrige una errata"]mensaje: corrige una errata¿es git? true¿tiene -m? trueorden para deshacer: ["guardar", "editar", "abrir"]split_first y split_last devuelven un Option, porque un slice vacío no tiene primer elemento. Por eso usamos if let.
Úsalo cuando necesites vecinos (windows), lotes (chunks) o “el primero y el resto” (split_first).
Métodos de iteradores que usarás todo el tiempo
Sección titulada «Métodos de iteradores que usarás todo el tiempo»La página de Iteradores explica map, filter, sum y collect. Aquí tienes las siguientes herramientas de la caja.
peekable: mira antes de tomar
Sección titulada «peekable: mira antes de tomar»Imagina que repartes cartas. A veces quieres ver la siguiente carta antes de decidir si la robas. .peekable() te da un iterador que puede hacer justo eso: .peek() te enseña el siguiente elemento sin sacarlo, y .next_if(...) lo saca solo si pasa una prueba. Es exactamente lo que necesitan los analizadores (parsers) pequeños.
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // Toma el siguiente carácter solo mientras también sea un dígito. while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}Salida:
["12", "+", "345", "-", "6"]Sin peekable, leerías el + mientras buscas más dígitos, y luego ya no estaría.
Úsalo cuando el siguiente elemento decide qué haces con el actual.
Recorrer listas: zip, enumerate, step_by, take_while, skip_while, chain
Sección titulada «Recorrer listas: zip, enumerate, step_by, take_while, skip_while, chain»| Método | Piénsalo como | Úsalo cuando… |
|---|---|---|
zip |
una cremallera que une dos lados diente a diente | tienes dos listas que van juntas |
enumerate |
numerar las líneas de una lista | necesitas el índice además del elemento |
step_by(n) |
subir la escalera de dos en dos (o de n en n) | quieres uno de cada n elementos |
take_while |
comer caramelos hasta dar con uno que no te gusta | quieres el principio de una lista, hasta el primer “no” |
skip_while |
saltarte los anuncios hasta que empieza el programa | quieres el resto, a partir del primer “no” |
chain |
enganchar dos vagones de tren | quieres una lista detrás de otra |
fn main() { let names = ["Ana", "Ben", "Chen"]; let scores = [88, 92, 75];
// zip empareja dos listas; enumerate añade un contador que empieza en 0 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by: cada 15 minutos dentro de una hora let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("cuartos de hora: {quarters:?}");
// into_iter() sobre un array da los propios números let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("antes del primero grande: {small:?}, de ahí en adelante: {rest:?}");
// chain: una lista detrás de la otra let morning = ["correo", "reunión diaria"]; let afternoon = ["revisión", "despliegue"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}Salida:
1. Ana: 882. Ben: 923. Chen: 75cuartos de hora: [0, 15, 30, 45]antes del primero grande: [3, 5, 8], de ahí en adelante: [12, 4]["correo", "reunión diaria", "revisión", "despliegue"]Fíjate en que take_while se detiene en el 12 para siempre. El 4 que viene después es pequeño, pero nunca llega a comprobarse. Esa es la diferencia con filter, que mira todos los elementos.
Responder preguntas: fold, position, find_map, partition, flat_map
Sección titulada «Responder preguntas: fold, position, find_map, partition, flat_map»| Método | Piénsalo como | Úsalo cuando… |
|---|---|---|
sum |
la tecla “+” de una calculadora | solo quieres el total |
fold |
una bola de nieve que rueda cuesta abajo y crece a cada paso | construyes un resultado a tu manera |
position |
“¿en qué asiento está Ben?” | necesitas el índice de la primera coincidencia |
find_map |
probar llaves hasta que una abre la puerta | pruebas algo con cada elemento y te quedas con el primer acierto |
partition |
separar la ropa sucia en dos cestos | divides una lista en dos con una pregunta de sí o no |
flat_map |
vaciar varias bolsas sobre una misma mesa | cada elemento se convierte en varios |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum: simplemente súmalos let total: i32 = orders.iter().sum();
// fold: súmalos a tu manera (aquí cada pedido paga además 2 euros de comisión) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("total {total}, con comisiones {with_fees}");
// position: el índice de la primera coincidencia println!("el primer pedido de más de 50 está en el índice {:?}", orders.iter().position(|p| *p > 50));
// partition: dos Vec, uno para el "sí" y otro para el "no" let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("grandes {big:?}, pequeños {small:?}");
// find_map: intenta convertir cada uno y quédate con el primero que funcione let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("primer número: {first_number:?}");
// flat_map: cada línea se convierte en varias palabras, todas en una lista let lines = ["comprar leche", "llamar a mamá"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}Salida:
total 144, con comisiones 154el primer pedido de más de 50 está en el índice Some(3)grandes [40, 55, 30], pequeños [12, 7]primer número: Some(42)["comprar", "leche", "llamar", "a", "mamá"]fold recibe un valor inicial (0) y una closure (función anónima) que recibe “el resultado hasta ahora” y el siguiente elemento. Si solo necesitas un total normal y corriente, sum lo dice con más claridad. Recurre a fold cuando la regla la pones tú.
Limpiar un Vec sin crear otro: dedup y retain
Sección titulada «Limpiar un Vec sin crear otro: dedup y retain»Estos dos son métodos de Vec, no de iteradores, pero van en el mismo cajón. dedup es como un DJ que nunca pone la misma canción dos veces seguidas (la canción puede volver más tarde). retain es el portero de una discoteca: solo se quedan dentro los elementos que pasan la prueba.
fn main() { // dedup quita las repeticiones que están una al lado de otra let mut clicks = vec!["inicio", "inicio", "tienda", "tienda", "tienda", "carrito", "inicio"]; clicks.dedup(); println!("{clicks:?}");
// retain se queda solo con los elementos que pasan la prueba let mut cart = vec![("manzana", 3), ("pan", 0), ("leche", 1), ("huevos", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}Salida:
["inicio", "tienda", "carrito", "inicio"][("manzana", 3), ("leche", 1)]El último "inicio" sobrevivió porque no estaba al lado de otro "inicio". Para quitar todos los duplicados, ordena primero, como muestra la página de Ordenación.
Úsalo cuando quieras cambiar un Vec sin construir uno nuevo.
Atajos de Option y Result
Sección titulada «Atajos de Option y Result»Un Option es como un paquete que puede haber llegado o no. Podrías abrir cada paquete con un match completo, pero casi siempre un ayudante de una sola palabra dice lo mismo. La página de Option explica unwrap_or y match; aquí tienes los siguientes ayudantes.
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default: el valor, o el valor por defecto del tipo ("", 0, Vec vacío...) println!("apodo: '{}'", nickname.unwrap_or_default());
// map_or: un valor de reserva si es None; si no, transforma el valor println!("{}", age.map_or("edad desconocida".to_string(), |a| format!("{a} años")));
// filter: se queda con el valor solo si pasa una prueba println!("edad de adulto: {:?}", age.filter(|a| *a >= 18));
// zip: Some solo si AMBOS son Some println!("área: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: encadena un paso que a su vez puede dar None let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("cuarenta").and_then(|s| s.parse::<u32>().ok()); println!("convertidos: {good:?} y {bad:?}");}Salida:
apodo: ''17 añosedad de adulto: Noneárea: Some(12)convertidos: Some(42) y NoneDentro de una función, ? también funciona con Option: si el valor es None, la función devuelve None en ese mismo momento. En una función que devuelve un Result, primero convierte el Option en un Result con ok_or(error) o ok_or_else(|| error). Usa ok_or_else cuando construir el error cueste trabajo, porque solo se ejecuta cuando hace falta.
struct Employee { name: String, manager: Option<String>,}
// `?` sobre un Option: si es None, devuelve None en ese momento.fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else convierte None en un error, así `?` funciona en una función que devuelve Result.fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "PORT no está definido".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) }; let grace = Employee { name: "Grace".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("ochenta")));
// transpose: Option<Result<..>> se convierte en Result<Option<..>> let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}Salida:
Ana: Some('G')Grace: NoneOk(8080)Err("PORT no está definido")Err("invalid digit found in string")Ok(Some(3))Err("invalid digit found in string") sale en inglés porque es el mensaje de error que trae la propia biblioteca estándar de Rust al convertir texto en número.
transpose se usa poco, pero es la respuesta elegante a “tengo un ajuste opcional y convertirlo puede fallar”. Con él puedes poner ? sobre el resultado y conservar el Option.
Úsalo cuando un match sobre un Option tiene una rama interesante y otra aburrida.
La API entry de HashMap
Sección titulada «La API entry de HashMap»Piensa en el guardarropa de un teatro. Dices un nombre y la persona encargada encuentra ese gancho o pone uno nuevo, y luego te lo da. Un solo viaje, no dos. Eso es .entry(key). La página de HashMaps muestra entry(key).or_insert(value), el contador clásico. Vale la pena conocer dos variantes más:
or_insert_with(f)solo construye el valor por defecto cuando falta la clave.or_default()hace lo mismo con el valor por defecto del tipo.and_modify(f)cambia el valor si la clave existe; encadenaor_insertdespués para el caso de “clave nueva”.
use std::collections::HashMap;
fn main() { let sales = [("manzanas", 3), ("peras", 2), ("manzanas", 5), ("ciruelas", 1), ("peras", 4)];
// and_modify + or_insert: actualiza si existe, inserta si no let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with: crea el Vec vacío solo cuando hace falta let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// El orden de un HashMap es aleatorio, así que ordena las claves antes de imprimir let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: total {}, pedidos {:?}", totals[fruit], orders[fruit]); }}Salida:
ciruelas: total 1, pedidos [1]manzanas: total 8, pedidos [3, 5]peras: total 6, pedidos [2, 4]Úsalo cuando de otro modo escribirías “si la clave existe, cámbiala; si no, insértala”, que busca la clave dos veces.
El tipo never !: código que nunca vuelve
Sección titulada «El tipo never !: código que nunca vuelve»Algunas calles son de sentido único: una vez que entras, ya no sales por donde viniste. En Rust, una expresión que nunca termina tiene el tipo never (el tipo “nunca”), que se escribe !. panic!, return, break, continue, un loop infinito y std::process::exit tienen todos este tipo.
Como un valor de tipo ! no puede llegar a existir, el compilador le deja ocupar el lugar de cualquier tipo. Por eso un match puede tener una rama que da un i32 y otra que hace return: la segunda rama nunca produce un valor, así que no puede romper los tipos.
use std::process;
// `-> !` significa "esta función nunca regresa"fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "fácil" => 10, "difícil" => 30, _ => return 0, // `return` tiene tipo `!`, así que encaja donde se espera un u32 }; points * 2}
fn main() { println!("fácil: {}, difícil: {}, raro: {}", level_points("fácil"), level_points("difícil"), level_points("raro"));
let mut total = 0; for input in ["4", "uy", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue` también es `!` }; total += n; } println!("total: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("no se encontró el archivo de configuración, me detengo"), }; println!("cargado {config}"); // nunca se imprime}Salida:
fácil: 20, difícil: 60, raro: 0total: 10no se encontró el archivo de configuración, me detengoUna advertencia: process::exit detiene el programa al instante, sin ejecutar el código de limpieza (los destructores) de los valores que siguen vivos. En programas reales, salir de main con un return normal suele ser la opción más amable.
Dos macros usan ! como marcadores de posición mientras haces un borrador del código. todo!() significa “esto lo escribo más tarde”. unimplemented!() significa “esto no está soportado aquí”. Las dos provocan un panic si alguna vez se llega a ellas, pero dejan que el resto de tu programa compile:
fn export_pdf(report: &str) -> Vec<u8> { todo!("la exportación a PDF llega en la v2")}Si la llamas, obtienes not yet implemented: la exportación a PDF llega en la v2. Con unimplemented!("exportación a PDF") el mensaje es not implemented: exportación a PDF. La primera parte sale en inglés porque la escribe Rust; tu texto va detrás.
Úsalo cuando una función deba detener el programa, o una rama deba salir antes de tiempo en lugar de dar un valor.
Pequeñas comodidades
Sección titulada «Pequeñas comodidades»Estas son las herramientas de bolsillo: diminutas, pero las usarás cada semana.
matches!(value, pattern)responde “¿encaja esto con el patrón?” contrueofalse(más en la página de Coincidencia de patrones).std::cmp::min/maxeligen el menor o el mayor de dos valores, y.clamp(low, high)mantiene un valor dentro de un rango, como un mando de volumen que se para en 100..abs_diff(other)da la distancia entre dos números. Siempre es positiva y nunca se desborda, ni siquiera con tipos sin signo.dbg!(expr)imprime el archivo, la línea, la expresión y el valor, y luego te devuelve el valor, así que puedes envolver cualquier cosa con él.assert!(condition)provoca un panic si la condición es falsa.debug_assert!hace lo mismo, pero solo en compilaciones de depuración (debug), así que no cuesta nada en las compilaciones de release.
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: comprobación de patrón de sí o no, con | para "o" let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("pedidos abiertos: {open}");
// min, max y clamp println!("mín {}, máx {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("volumen {volume} limitado a {safe}");
// abs_diff: a qué distancia están, nunca negativo let (guess, secret): (u32, u32) = (30, 42); println!("estás a {} de distancia", guess.abs_diff(secret));
// dbg! imprime en stderr y te devuelve el valor let doubled = dbg!(safe * 2);
// assert! detiene el programa si esto llega a ser falso assert!(doubled <= 200, "volumen doble demasiado alto: {doubled}"); debug_assert!(safe <= 100); // solo se comprueba en compilaciones debug println!("todas las comprobaciones pasaron");}Salida:
pedidos abiertos: 3mín 3, máx 7volumen 140 limitado a 100estás a 12 de distanciatodas las comprobaciones pasaronLa línea de dbg! no aparece en la salida de arriba, porque dbg! escribe en stderr (la salida de errores), no en stdout. En el Playground aparece en el panel “Standard Error”:
[src/main.rs:28:19] safe * 2 = 200Por eso también puedes repartir dbg! sin miedo mientras depuras: no se mezcla con la salida real de tu programa. Eso sí, acuérdate de quitarlo antes de publicar.
La ordenación tiene sus propias herramientas pequeñas. La página de Ordenación las explica a fondo; esta es la versión corta. sort_unstable es un poco más rápido que sort y va perfecto para números simples. sort_by_key ordena por un campo. binary_search encuentra un elemento en datos ordenados muy deprisa, como abrir un diccionario por la mitad en lugar de leerlo desde la primera página.
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable: algo más rápido; los elementos iguales pueden cambiar de sitio let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search: Ok(índice) si lo encuentra, Err(índice) donde iría println!("buscar 19: {:?}", ids.binary_search(&19)); println!("buscar 20: {:?}", ids.binary_search(&20));
// sort_by_key: ordena por un campo let mut players = vec![ Player { name: "Ana", score: 120 }, Player { name: "Ben", score: 95 }, Player { name: "Chen", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("de menor a mayor: {names:?}");}Salida:
[3, 7, 19, 25, 42]buscar 19: Ok(2)buscar 20: Err(3)de menor a mayor: ["Ben", "Ana", "Chen"]Err(3) no es un fallo del que preocuparse. Te dice que el 20 no está en la lista y que el índice 3 es donde podrías insertarlo para que la lista siga ordenada.
Úsalo cuando te descubras escribiendo un pequeño if o un bucle que una de estas herramientas ya hace por ti.
Errores comunes
Sección titulada «Errores comunes»Usar mem::take con un tipo que no implementa Default. mem::take tiene que dejar algo en su lugar, así que necesita saber qué significa “vacío” para ese tipo:
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ ¿cómo sería una Connection "vacía"? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ ¿cómo sería una Connection "vacía"? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`El compilador dice (en inglés) que Connection no implementa el trait Default, y sugiere añadirle #[derive(Default)]. Arréglalo eligiendo tú el reemplazo con mem::replace(&mut self.active, Connection { host: "db2".to_string() }), o guardando el campo como Option<Connection> y usando self.active.take(). Añade #[derive(Default)] solo si un valor “vacío” tiene sentido de verdad para ese tipo.
Usar ? sobre un Option en una función que devuelve Result. Los dos no se mezclan solos:
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None no tiene ningún mensaje de error que devolver raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ None no tiene ningún mensaje de error que devolver | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`El compilador ya te da la solución: setting.ok_or("PORT no está definido".to_string())?, o ok_or_else como en el ejemplo de arriba. Consulta E0277 para ver otras formas de este error.
Esperar que dedup o binary_search funcionen con datos sin ordenar. dedup solo quita las repeticiones que están una al lado de otra, y binary_search sobre datos desordenados da una respuesta sin sentido. Ninguno de los dos es un error de compilación, así que es fácil que se te pase. Ordena primero.
Olvidar los sobrantes de chunks_exact. Un bucle for sobre chunks_exact(3) se salta en silencio los últimos 1 o 2 elementos. Si te importan, trata .remainder() o usa chunks en su lugar.
Práctica
Sección titulada «Práctica»1. Cambios de precio diarios
Sección titulada «1. Cambios de precio diarios»Tienes los precios [100, 104, 98, 110, 107]. Construye un Vec con el cambio de cada día al siguiente, imprímelo y luego imprime la mayor subida. Pista: windows(2) te da cada pareja como un pequeño slice w, así que el cambio es w[1] - w[0]. .iter().max() encuentra el mayor.
Ver solución
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("cambios: {changes:?}");
let biggest_rise = changes.iter().max(); println!("mayor subida: {biggest_rise:?}");}Salida:
cambios: [4, -6, 12, -3]mayor subida: Some(12)Cinco precios dan cuatro cambios, porque windows(2) forma una pareja por cada paso. max() devuelve un Option, ya que una lista vacía no tiene un valor mayor.
2. Vacía la cola de impresión
Sección titulada «2. Vacía la cola de impresión»Escribe un struct Printer con un campo queue: Vec<String>. Dale un método add(&mut self, file: &str) y un método flush(&mut self) -> Vec<String> que devuelva todos los archivos de la cola y la deje vacía. Añade dos archivos, vacía la cola e imprime lo que salió y cuántos archivos quedan. Pista: flush solo tiene &mut self, así que usa std::mem::take.
Ver solución
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("informe.pdf"); printer.add("foto.png");
let jobs = printer.flush(); println!("imprimiendo {} trabajos: {jobs:?}", jobs.len()); println!("quedan en la cola: {}", printer.queue.len());}Salida:
imprimiendo 2 trabajos: ["informe.pdf", "foto.png"]quedan en la cola: 0Escribir self.queue a secas en flush fallaría con E0507. mem::take saca el Vec y deja uno vacío en su lugar, sin copiar nada.
3. El primer puerto válido
Sección titulada «3. El primer puerto válido»Escribe una función first_port(candidates: &[&str]) -> Option<u16> que devuelva la primera cadena de texto que se pueda convertir en un u16. Llámala con ["", "abc", "70000", "8080", "9090"] y con ["ninguno", "-1"], e imprime el puerto, o 3000 si no hay ninguno. Pista: find_map con s.parse::<u16>().ok(), y luego unwrap_or(3000).
Ver solución
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["ninguno", "-1"];
println!("usando el puerto {}", first_port(&from_config).unwrap_or(3000)); println!("usando el puerto {}", first_port(&all_bad).unwrap_or(3000));}Salida:
usando el puerto 8080usando el puerto 3000"70000" parece un número, pero no cabe en un u16 (el máximo es 65535), así que la conversión falla y find_map pasa al siguiente. Se detiene en "8080" y ni siquiera llega a mirar "9090".
Resumen
Sección titulada «Resumen»mem::take,mem::replaceyOption::takesacan un valor de un&mutdejando algo en su lugar;mem::swapintercambia dos valores.- En los slices,
windowsda vecinos solapados,chunksda lotes, ysplit_first/split_lastdan “un elemento y el resto”. peekablete deja mirar por adelantado;zip,enumerate,chain,fold,find_mapypartitionsustituyen muchos bucles escritos a mano.- Los ayudantes de
Optioncomounwrap_or_default,map_or,filteryok_orte ahorran escribir pequeñosmatch, y?también funciona conOption. entry()conor_insert_withoand_modifyactualiza unHashMapcon una sola búsqueda.- El tipo never
!es el del código que nunca termina (panic!,return,continue,process::exit), así que encaja en cualquier sitio donde se espera un valor.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué deja mem::take(&mut self.items) en su lugar cuando items es un Vec?
2. ¿Qué te da [1, 2, 3, 4].windows(2)?
3. ¿Por qué compila let x: i32 = match s { "a" => 1, _ => return };?
4. Tu función devuelve un Result, pero tienes un Option. ¿Qué añades antes de ?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»std::mem— documentación de la biblioteca estándar (en inglés):take,replace,swapy las demás funciones de memoria, con ejemplos.- Métodos de slice — documentación de la biblioteca estándar (en inglés): todos los métodos de los slices, incluidos
windows,chunks,split_firstybinary_search. - El trait
Iterator— documentación de la biblioteca estándar (en inglés): la lista completa de métodos de los iteradores, desdezipypeekablehastapartitionyfind_map. Option— documentación de la biblioteca estándar (en inglés): todos los ayudantes deOption, agrupados según lo que hacen.- Funciones divergentes — Rust by Example (en inglés): ejemplos cortos de funciones que devuelven
!y por qué es útil.