Ir al contenido

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 (un Vec vacío, un String vacío, 0, None).
  • mem::replace(&mut x, new) pone new en su lugar y te devuelve el valor antiguo.
  • Option::take() se lleva lo que hay dentro de un Option y deja None.
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: None
▶ Pruébalo tú mismo

No 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")
▶ Pruébalo tú mismo

Úsalo cuando solo tienes acceso &mut pero necesitas quedarte con el valor que hay dentro.

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
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 3
15 -> 14: cambio -1
14 -> 19: cambio 5
19 -> 21: cambio 2
pareja [1, 2]
pareja [3, 4]
sobrante: [5]
▶ Pruébalo tú mismo

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? true
orden para deshacer: ["guardar", "editar", "abrir"]
▶ Pruébalo tú mismo

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.

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"]
▶ Pruébalo tú mismo

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: 88
2. Ben: 92
3. Chen: 75
cuartos 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"]
▶ Pruébalo tú mismo

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 154
el 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á"]
▶ Pruébalo tú mismo

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)]
▶ Pruébalo tú mismo

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.

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ños
edad de adulto: None
área: Some(12)
convertidos: Some(42) y None
▶ Pruébalo tú mismo

Dentro 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: None
Ok(8080)
Err("PORT no está definido")
Err("invalid digit found in string")
Ok(Some(3))
▶ Pruébalo tú mismo

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.

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; encadena or_insert despué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]
▶ Pruébalo tú mismo

Úsalo cuando de otro modo escribirías “si la clave existe, cámbiala; si no, insértala”, que busca la clave dos veces.

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: 0
total: 10
no se encontró el archivo de configuración, me detengo
▶ Pruébalo tú mismo

Una 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.

Estas son las herramientas de bolsillo: diminutas, pero las usarás cada semana.

  • matches!(value, pattern) responde “¿encaja esto con el patrón?” con true o false (más en la página de Coincidencia de patrones).
  • std::cmp::min / max eligen 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: 3
mín 3, máx 7
volumen 140 limitado a 100
estás a 12 de distancia
todas las comprobaciones pasaron
▶ Pruébalo tú mismo

La 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 = 200

Por 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"]
▶ Pruébalo tú mismo

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.

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.

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)
▶ Pruébalo tú mismo

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.

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: 0
▶ Pruébalo tú mismo

Escribir 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.

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 8080
usando el puerto 3000
▶ Pruébalo tú mismo

"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".

  • mem::take, mem::replace y Option::take sacan un valor de un &mut dejando algo en su lugar; mem::swap intercambia dos valores.
  • En los slices, windows da vecinos solapados, chunks da lotes, y split_first / split_last dan “un elemento y el resto”.
  • peekable te deja mirar por adelantado; zip, enumerate, chain, fold, find_map y partition sustituyen muchos bucles escritos a mano.
  • Los ayudantes de Option como unwrap_or_default, map_or, filter y ok_or te ahorran escribir pequeños match, y ? también funciona con Option.
  • entry() con or_insert_with o and_modify actualiza un HashMap con 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 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