Guía de respuestas y soluciones razonadas: Módulo 01 — Rust#
Esta guía de referencia técnica presenta las soluciones canónicas, el proceso de razonamiento paso a paso bajo la disciplina TDD (RED arrow GREEN arrow REFACTOR), el análisis exhaustivo de diseño, las respuestas completas a las preguntas de diagnóstico socrático y los contraejemplos de errores de compilación para las lecciones 01 a 04 del módulo de fundamentos de Rust.
Intenta resolver primero cada ejercicio por tu cuenta y registrar la evidencia de tu fallo inicial (fase RED) antes de leer esta guía. El aprendizaje profundo en sistemas no proviene de leer código ajeno terminado, sino de enfrentar los diagnósticos del compilador y razonar sobre el ciclo de vida y la memoria de tus datos.
Para profundizar en cualquier término técnico utilizado en estas soluciones, consulta el glosario técnico.
Lección 01 — Tipos y flujo de control (shipping_fee)#
1. Razonamiento paso a paso y estrategia TDD (RED arrow GREEN arrow REFACTOR)#
Declaración formal del contrato y regla de dominio:#
Se requiere una función pura shipping_fee(total_cents: u32, is_member: bool) -> u32 que calcule el costo de envío en centavos enteros para evitar los errores de redondeo de punto flotante binario (IEEE 754):
- Umbral de gratuidad: $5000$ centavos ($50.00 dólares).
- Tarifa estándar: $500$ centavos ($5.00 dólares).
- Regla de miembros: Si el cliente tiene membresía (
is_member == true), el costo es $0$ centavos, sin importar el monto total de la orden. - Regla de no miembros: Si el cliente no tiene membresía, el costo es $0$ centavos si
total_cents >= 5000, y $500$ centavos sitotal_cents < 5000.
Matriz de valores límite y casos de frontera:#
- Caso 1: Orden de $0$ centavos, no miembro arrow
500. - Caso 2: Orden de $4999$ centavos (justo un centavo antes del umbral), no miembro arrow
500. - Caso 3: Orden de $5000$ centavos (exactamente en el umbral), no miembro arrow
0. - Caso 4: Orden de $5001$ centavos (justo un centavo sobre el umbral), no miembro arrow
0. - Casos 5-8: Mismos montos monetarios ($0$, $4999$, $5000$, $5001$), pero con
is_member == truearrow Todos deben retornar0.
Ciclo TDD:#
- Fase RED: Escribimos la firma de la función devolviendo un valor deliberadamente erróneo (por ejemplo,
999otodo!()) y añadimos la suite de pruebas completa enmod tests. Ejecutamosrustc --test shipping.rsy verificamos que el compilador reporta un fallo de aserción. - Fase GREEN: Implementamos la lógica mínima para hacer pasar las pruebas:
``
rust if is_member || total_cents >= 5000 { 0 } else { 500 }`` - Fase REFACTOR: Extraemos los literales mágicos a constantes con nombres de dominio expresivos (
const FREE_SHIPPING_THRESHOLD_CENTS: u32 = 5000;,const STANDARD_FEE_CENTS: u32 = 500;).
2. Implementación canónica completa y ejecutable#
El siguiente bloque es un programa autónomo y ejecutable con pruebas. Puedes guardarlo como lesson01_solution.rs y verificarlo con rustc --test lesson01_solution.rs:
// Contract: Calculates the shipping fee in whole integer cents.
// Free shipping applies if the customer is a registered member or if
// the total purchase meets or exceeds the defined threshold of 5000 cents ($50.00).
pub fn shipping_fee(total_cents: u32, is_member: bool) -> u32 {
const FREE_SHIPPING_THRESHOLD_CENTS: u32 = 5000;
const STANDARD_FEE_CENTS: u32 = 500;
if is_member || total_cents >= FREE_SHIPPING_THRESHOLD_CENTS {
0
} else {
STANDARD_FEE_CENTS
}
}
#[cfg(test)]
mod tests {
use super::shipping_fee;
#[test]
fn calculates_shipping_fee_across_all_boundaries() {
// Non-member boundary tests
assert_eq!(shipping_fee(0, false), 500);
assert_eq!(shipping_fee(4999, false), 500);
assert_eq!(shipping_fee(5000, false), 0);
assert_eq!(shipping_fee(5001, false), 0);
// Member tests (membership guarantees free shipping regardless of amount)
assert_eq!(shipping_fee(0, true), 0);
assert_eq!(shipping_fee(4999, true), 0);
assert_eq!(shipping_fee(5000, true), 0);
assert_eq!(shipping_fee(5001, true), 0);
}
}
Justificación de decisiones de diseño:#
- Uso de
u32en lugar def64: Impide números negativos y elimina cualquier discrepancia por redondeo binario. - Expresión condicional pura: Se utiliza
if/elsecomo expresión que produce un valor directo, eliminando la necesidad de variables intermedias mutables o sentenciasreturnredundantes. - Operador de cortocircuito
||: Siis_memberestrue, el procesador no necesita realizar la comparación numéricatotal_cents >= 5000, optimizando el salto condicional.
3. Respuestas a las preguntas de comprensión profunda (Lección 01)#
Pregunta 1: La semántica del punto y coma#
result_aevalúa a20de tipo escalaru32. La última línea del bloque carece de punto y coma, actuando como una expresión de cola (tail expression) que entrega su valor al binding exterior.result_bevalúa al tipo unitario()con valor(). Al colocar un punto y coma (;), transformamos la expresión en una instrucción de expresión (expression statement), descartando el resultado del cálculo.- Razón de diseño: En Rust, todo tiene un tipo. Si el punto y coma no descartara el valor, no habría forma sintáctica clara de ejecutar una acción por sus efectos secundarios (como escribir en disco o imprimir) sin forzar a que ese valor contamine el tipo de retorno del bloque. Al hacer que
;retorne(), Rust logra un lenguaje de expresiones uniforme y matemáticamente riguroso.
Pregunta 2: Coerción de verdad (Truthy/Falsy) y solidez de tipos#
- El compilador emite el error
E0308(mismatched types: expectedbool, foundu32). - Riesgo en pasarelas bancarias: En la convención de POSIX y C, el código de estado
0significaEXIT_SUCCESS(éxito), mientras que números distintos de cero representan diversos errores (1= timeout,2= fondos insuficientes). Si Rust permitiera coerción implícita booleana donde0es falso y cualquier número positivo es verdadero, escribirif status_code { /* reintentar */ }trataría el éxito como falso y un fallo crítico como verdadero. La exigencia estricta de Rust de escribirif status_code == 0erradica ambigüedades lógicas en sistemas financieros.
Pregunta 3: Unificación estática en expresiones condicionales#
- Rust rechaza
let code = if active { 1_u32 };porque toda variable debe tener un tipo y tamaño exacto determinado estáticamente en tiempo de compilación. - La ausencia de una cláusula
elseasigna implícitamente el tipo unitario()a la rama alternativa:else { () }. - El compilador intenta unificar los tipos de ambas ramas: la rama
ifproduceu32y la ramaelseproduce(). Al ser tipos totalmente incompatibles, el compilador aborta conE0308. El compilador no puede emitir un binario donde una variable contengau32si una condición de tiempo de ejecución es verdadera y()si es falsa.
Pregunta 4: Pérdida de precisión en aritmética de punto flotante#
- En el estándar IEEE 754 de 64 bits (
f64), los números se descomponen en 1 bit de signo, 11 bits de exponente y 52 bits de mantisa (fracción binaria). El número $0.1$ en base 10 equivale a 1/10, el cual en base 2 es un número periódico infinito (0.0001100110011...₂). Al tener una mantisa finita, se trunca, produciendo 0.10000000000000000555... y $0.2$ produce 0.20000000000000001110.... Al sumarlos, el resultado es 0.3000000000000000444..., haciendo que0.1 + 0.2 == 0.3sea falso. - En un sistema de 100,000 transacciones diarias, ese error infinitesimal se acumula en las reconciliaciones bancarias, generando descuadres contables mensuales de miles de centavos. Representar el dinero en centavos enteros con
u32ou64opera en aritmética entera exacta de CPU, erradicando este problema por diseño.
Pregunta 5: Evaluación de cortocircuito y contratos de dominio#
- La CPU evalúa las condiciones estrictamente de izquierda a derecha: primero
owner_id == actor_id, y si resulta verdadera, procede a evaluar!archived. - Si
owner_id == actor_ides falso, la operación lógica&&concluye inmediatamente con resultadofalse, omitiendo por completo la lectura de la memoria o registro de!archived. - Si la segunda condición fuera una consulta pesada a base de datos (
is_locked_in_db()), ubicar primero la comprobación de identificadores en memoria asegura que el 99% de las peticiones no autorizadas sean rechazadas en menos de 2 nanosegundos en CPU sin saturar la red ni la base de datos.
4. Errores comunes y contraejemplos (Lección 01)#
Error común: Ramas redundantes y anidamiento innecesario#
// Código defectuoso por complejidad innecesaria
pub fn shipping_fee_bad(total_cents: u32, is_member: bool) -> u32 {
if is_member {
0
} else {
if total_cents >= 5000 {
0
} else {
500
}
}
}
Problema: Introduce saltos visuales innecesarios y aumenta la complejidad ciclomática cuando una simple disyunción lógica (is_member || total_cents >= 5000) resuelve la regla limpiamente.
Contraejemplo deliberado: Error E0308#
fn main() {
let allowed = true;
let result = if allowed { 1_u32 } else { false };
println!("{result}");
}
Diagnóstico del compilador:
error[E0308]: `if` and `else` have incompatible types
--> main.rs:3:46
|
3 | let result = if allowed { 1_u32 } else { false };
| ----- ^^^^^ expected `u32`, found `bool`
Lección 02 — Ownership y préstamos (count_matching y experimento E0502)#
1. Razonamiento paso a paso y estrategia TDD (RED arrow GREEN arrow REFACTOR)#
Declaración del contrato:#
La función count_matching(items: &[String], prefix: &str) -> usize debe contar cuántas cadenas en una colección comienzan con un prefijo dado:
- No debe transferir propiedad de la colección (debe tomar un slice prestado
&[String]). - No debe clonar cadenas ni reservar nuevos búferes en el heap.
- Un prefijo vacío
""debe coincidir con todas las cadenas de la colección. - Una colección vacía debe devolver
0.
Ciclo TDD:#
- Fase RED: Escribimos la firma con
todo!()y la batería de pruebas enmod tests. Comprobamos el fallo conrustc --test. - Fase GREEN: Implementamos el pipeline funcional con
.iter(),.filter()y.count(). - Fase REFACTOR: Comprobamos que el paso de parámetros aprovecha los slices prestados sin clonar.
2. Implementación canónica completa y ejecutable#
// Contract: Counts how many strings in the borrowed slice start with the specified prefix.
// Borrows both inputs; performs zero heap allocations.
pub fn count_matching(items: &[String], prefix: &str) -> usize {
items
.iter()
.filter(|item| item.starts_with(prefix))
.count()
}
#[cfg(test)]
mod tests {
use super::count_matching;
#[test]
fn counts_matching_prefixes_accurately() {
let items = vec![
String::from("notes"),
String::from("task"),
String::from("notebook"),
];
// Standard prefix matching
assert_eq!(count_matching(&items, "no"), 2);
// Empty prefix matches every string in the collection
assert_eq!(count_matching(&items, ""), 3);
// Absent prefix matches zero strings
assert_eq!(count_matching(&items, "xyz"), 0);
// Empty collection matches zero strings
let empty_list: [String; 0] = [];
assert_eq!(count_matching(&empty_list, "no"), 0);
}
}
Justificación técnica de diseño:#
items: &[String]: Permite pasar un&Vec<String>, un arreglo fijo&[String; 3]o un sub-slice&items[1..2], otorgando máxima flexibilidad al llamador sin forzar la titularidad de unVec.prefix: &str: Recibe una vista de cadena sin exigir que el llamador cree unStringen el heap para la búsqueda.- Rendimiento: Complejidad espacial O(1) adicional. Solo recorre las cadenas ya existentes en memoria.
3. Experimento de laboratorio: Conflicto de préstamos e invalidación (E0502)#
Para comprender físicamente por qué Rust prohíbe solapar préstamos compartidos y mutables, analizamos este programa deliberadamente erróneo:
fn main() {
let mut numbers = vec![1, 2, 3];
let first = &numbers[0]; // Immutable borrow occurs here
numbers.push(4); // Mutable borrow occurs here
println!("{first}"); // Immutable borrow later used here
}
Diagnóstico exacto de rustc:#
error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
--> main.rs:4:5
|
3 | let first = &numbers[0];
| ------- immutable borrow occurs here
4 | numbers.push(4);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{first}");
| ----- immutable borrow later used here
Explicación física del fallo de seguridad prevenido:#
Supongamos que el vector numbers tenía una capacidad inicial de 3 elementos en el heap (en la dirección 0x1000).
let first = &numbers[0];almacena un puntero directo a la dirección física0x1000.- Al invocar
numbers.push(4), el vector detecta que está lleno (len == cap == 3). - Para insertar el cuarto elemento, el asignador de memoria reserva un nuevo bloque más grande en otra dirección (por ejemplo,
0x2000), copia los datos1, 2, 3, añade4y libera el bloque antiguo en0x1000. - Si la línea 5 intentara leer
*first, estaría desreferenciando la dirección0x1000, la cual ya fue devuelta al sistema operativo: ¡un *puntero colgante (dangling pointer) y vulnerabilidad de use-after-free*! - El analizador de préstamos de Rust previene esto de forma estática en compilación emitiendo
E0502.
4. Respuestas a las preguntas de comprensión profunda (Lección 02)#
Pregunta 1: Traza de memoria en Stack y Heap#
- Tras
let s1 = String::from("rust");, en una máquina de 64 bits: - En la pila (stack): El descriptor de
s1ocupa 24 bytes: puntero de 8 bytes (por ejemplo0x5000), capacidad de 8 bytes (4), y longitud de 8 bytes (4). - En el montículo (heap): En la dirección
0x5000residen 4 bytes con los valores ASCII/UTF-8[b'r', b'u', b's', b't']. - Con
let s2 = s1;, se copian superficialmente los 24 bytes de la pila des1as2. No se reserva ni un solo byte nuevo en el heap. - El compilador invalida lógicamente a
s1. Si permitiera que ambos bindings siguieran activos, ambos invocaríanDropal terminar su ámbito, causando una liberación doble de memoria (double-free) de la dirección0x5000.
Pregunta 2: La incompatibilidad entre Copy y Drop#
- Si un tipo implementara simultáneamente
CopyyDrop, cada vez que el tipo se pase a una función o se asigne a otra variable, se duplicarían sus bits en la pila. Al salir de cada ámbito, el destructorDropse ejecutaría una vez por cada copia creada. Si el tipo poseyera un recurso externo (un socket de red, un descriptor de archivo o memoria en el heap), se intentaría cerrar o liberar múltiples veces el mismo identificador del sistema operativo. - Por tanto, Rust impone como regla de lenguaje: si un tipo implementa
Drop, está inhabilitado para implementarCopy. Tipos como[u8; 32]o(i32, bool)viven 100% en la pila y no gestionan recursos dinámicos, por lo que duplicar sus bits conmemcpyes totalmente seguro.
Pregunta 3: Anatomía de Fat Pointers y Slices#
- En arquitectura de 64 bits:
text(String): 24 bytes (puntero, capacidad, longitud).r_text(&String): 8 bytes (un puntero simple que apunta a la estructura de 24 bytes en el stack).slice(&str): 16 bytes (un fat pointer compuesto por un puntero directo al inicio del texto y la longitud en bytes).- El tipo
strsin referencia es un tipo de tamaño dinámico (Dynamically Sized Type, DST). Como una cadena de texto puede tener cualquier cantidad arbitraria de bytes desconocida en compilación, el procesador no puede saber cuántos bytes reservar en el marco de pila paralet x: str;. Por ello, en Rust los tipos DST solo pueden utilizarse detrás de un puntero que almacene la longitud, como el fat pointer&str. - La coerción por desreferenciación (deref coercion) permite que cuando pasas
&Stringa una función que espera&str, el compilador transforme automáticamente la referencia de 8 bytes en el fat pointer de 16 bytes leyendo el puntero y la longitud del descriptor.
Pregunta 4: Aliasing y mutabilidad en un solo hilo#
- Si en C o C++
const int* currentyint* targetapuntan a la misma dirección de memoria (aliasing), escribir a través detargetmodifica el valor leído porcurrent. Esto invalida las optimizaciones del compilador, ya que no puede mantener el valor en un registro de CPU y debe recargarlo continuamente de la memoria RAM por temor a que haya sido mutado. - La garantía de Rust ("muchos lectores O un solo escritor exclusivo") asegura que si existe una referencia compartida
&T, ningún puntero en todo el programa puede mutar ese dato. Esto permite al optimizador LLVM aplicar transformaciones agresivas de vectorización y almacenamiento en registros con total seguridad matemática.
Pregunta 5: Elisión de Lifetimes e invariantes de función#
create_greetingfalla porque intenta devolver&s, una referencia alStringlocals.- Cuando la función termina su ejecución, el marco de pila se destruye y el compilador inserta automáticamente la llamada a
Drop::drop(&mut s), liberando el búfer del heap. Si se permitiera devolver la referencia, quien la reciba tendría un puntero a memoria liberada. - Para corregirlo, la función debe transferir la propiedad devolviendo el contenedor poseído directamente:
fn create_greeting() -> String.
5. Errores comunes y contraejemplos (Lección 02)#
Contraejemplo deliberado: Error E0382#
fn main() {
let first = String::from("note");
let second = first; // Move occurs here
println!("{first}"); // Illegal access
}
Diagnóstico del compilador:
error[E0382]: borrow of moved value: `first`
--> main.rs:4:16
|
2 | let first = String::from("note");
| ----- move occurs because `first` has type `String`, which does not implement the `Copy` trait
3 | let second = first;
| ----- value moved here
4 | println!("{first}");
| ^^^^^ value borrowed here after move
Lección 03 — Modelar errores y escribir pruebas (parse_priority)#
1. Razonamiento paso a paso y estrategia TDD (RED arrow GREEN arrow REFACTOR)#
Declaración del dominio:#
Se requiere parsear una entrada de texto a una prioridad numérica de tipo u8 en el rango cerrado de 1 a 5:
- Espacios en blanco exteriores deben descartarse con
.trim(). - Si tras el recorte la cadena está vacía, debe retornar
Err(PriorityError::Empty). - Si no es un número entero válido de 8 bits, debe retornar
Err(PriorityError::InvalidNumber). - Si es un número pero queda fuera del intervalo [1, 5], debe retornar
Err(PriorityError::OutOfRange(valor_rechazado)).
Ciclo TDD:#
- Fase RED: Definimos el enum
PriorityErrorcon#[derive(Debug, PartialEq, Eq)]. Escribimos la funciónparse_priorityretornandotodo!(). Creamos la batería de pruebas cubriendo casos vacíos, no numéricos, desbordamientos, límites exactos (0, 1, 5, 6) y valores con espacios. Verificamos el fallo conrustc --test. - Fase GREEN:
```rust
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(PriorityError::Empty);
}
let parsed = trimmed
.parse::
() .map_err(|_| PriorityError::InvalidNumber)?;
match parsed { 1..=5 => Ok(parsed), other => Err(PriorityError::OutOfRange(other)), }
3. **Fase REFACTOR:** Comprobamos la exhaustividad del patrón de coincidencia y la ausencia total de `.unwrap()`.
---
### 2. Implementación canónica completa y ejecutable
// Domain Error: Categorizes the possible failure modes when parsing a priority level.
pub enum PriorityError { Empty, InvalidNumber, OutOfRange(u8), }
// Contract: Parses a text slice into a priority level between 1 and 5 inclusive. // Trims whitespace before validation. pub fn parse_priority(input: &str) -> Result
if trimmed.is_empty() { return Err(PriorityError::Empty); }
let parsed = trimmed .parse::
match parsed { 1..=5 => Ok(parsed), other => Err(PriorityError::OutOfRange(other)), } }
mod tests { use super::{parse_priority, PriorityError};
fn parses_priority_levels_and_handles_all_error_variants() { // Empty strings assert_eq!(parse_priority(" "), Err(PriorityError::Empty)); assert_eq!(parse_priority(""), Err(PriorityError::Empty));
// Invalid non-numeric input assert_eq!(parse_priority("urgent"), Err(PriorityError::InvalidNumber));
// Numeric overflow for u8 (> 255) assert_eq!(parse_priority("999"), Err(PriorityError::InvalidNumber));
// Out of range lower boundary (< 1) assert_eq!(parse_priority("0"), Err(PriorityError::OutOfRange(0)));
// Valid boundaries and trimmed values assert_eq!(parse_priority("1"), Ok(1)); assert_eq!(parse_priority(" 3 "), Ok(3)); assert_eq!(parse_priority("5"), Ok(5));
// Out of range upper boundary (> 5) assert_eq!(parse_priority("6"), Err(PriorityError::OutOfRange(6))); } }
#### Justificación técnica de diseño:
- **`OutOfRange(u8)` con carga útil:** Permite al llamador saber exactamente qué número inválido intentó ingresar el usuario para reportarlo en telemetría o en una respuesta HTTP detallada sin recurrir a formateo de cadenas frágiles.
- **Uso de `.map_err()` y `?`:** Transforma el error técnico `ParseIntError` a nuestro tipo de dominio antes de propagarlo de inmediato si el texto no era numérico.
---
### 3. Respuestas a las preguntas de comprensión profunda (Lección 03)
#### Pregunta 1: Desazucarización y tipo de retorno con `?`
- El operador `?` no compila dentro de una función con retorno unitario `fn print_user_age(input: &str)` porque se desazucara en una sentencia `return Err(...)`. Si la función contenedora no declara un tipo de retorno compatible con `Result`, el compilador detecta incompatibilidad de tipos (`E0308`: la función promete devolver `()`, pero el operador `?` intenta devolver `Result`).
- El operador `?` exige que la función retorne `Result<T, E>` o cualquier tipo que implemente el trait `Try`.
#### Pregunta 2: Sobrecoste en tiempo de ejecución: Excepciones vs `Result`
- En la JVM o .NET, lanzar una excepción implica pausar la ejecución del hilo para inspeccionar la pila de llamadas, recopilar información de nombres de clases y números de línea para construir el `StackTrace`, y consultar tablas de desenrollado de pila (*exception tables*) en memoria, costando miles de ciclos de CPU por cada excepción lanzada.
- En Rust, `Result<u8, PriorityError>` es un tipo suma que ocupa típicamente 2 bytes contiguos en la pila (1 byte para el discriminante y 1 byte para el valor/error). Retornarlo cuesta lo mismo que retornar un entero en un registro de CPU (`RAX` en x86_64). Evaluar un error toma una fracción de nanosegundo mediante una simple instrucción de salto condicional (`test`/`jnz`).
#### Pregunta 3: Requisitos de traits en el arnés de pruebas
- Si eliminas `#[derive(Debug, PartialEq)]`:
1. Fallará por falta del trait `PartialEq`: la macro `assert_eq!` no podrá compilar la comparación binaria `left == right`.
2. Fallará por falta del trait `Debug`: si la aserción falla, la macro necesita imprimir ambos lados en consola utilizando el especificador `{:?}`.
- `PartialEq` proporciona el método `eq(&self, other: &Self) -> bool`. `Debug` proporciona el método `fmt(&self, f: &mut Formatter) -> fmt::Result`.
#### Pregunta 4: La frontera arquitectónica entre `Option`, `Result` y `panic!`
- **`Ok(None)`:** Cuando la ausencia del dato es un resultado normal de negocio (por ejemplo, buscar un usuario por ID en la base de datos y comprobar que no existe). Retornar `None` permite a la capa web devolver un `404 Not Found` ordenado.
- **`Err(DatabaseError)`:** Cuando la operación esperada no pudo completarse debido a una contingencia del entorno (por ejemplo, timeout de red o credencial rechazada), permitiendo a la capa web responder un `503 Service Unavailable`.
- **`panic!`:** Cuando se ha violado una invariante de programación que indica que el estado de memoria del driver o del proceso está corrupto y continuar ejecutando pondría en riesgo la integridad de los datos.
#### Pregunta 5: Errores tipados con `enum` vs cadenas `String`
- Si la función devuelve `Result<TransactionId, String>`, la interfaz de usuario no tiene forma confiable de internacionalizar el mensaje: si el backend devuelve `"insufficient funds"`, el frontend tendría que hacer comparaciones frágiles de cadenas. Si el desarrollador backend corrige la ortografía a `"insufficient balance"`, el frontend se rompe silenciosamente.
- Con un `enum PaymentError { InsufficientFunds { available: u32 }, ExpiredCard }`, el frontend puede realizar un `match` exhaustivo garantizado por el compilador, mostrando el texto adecuado en cualquier idioma sin acoplamiento a mensajes de depuración.
---
### 4. Errores comunes y contraejemplos (Lección 03)
#### Error común: Usar números centinela como indicadores de error
// Antiprón: Devolver 0 para indicar error pub fn parse_priority_bad(input: &str) -> u8 { match input.trim().parse::
*Problema:* Fuerza al llamador a recordar que el número 0 es un valor mágico prohibido. Si el llamador olvida comprobar el 0 y lo almacena en la base de datos, corrompe las invariantes de negocio. El uso de `Result` hace imposible olvidar el manejo de error.
---
## Lección 04 — Colecciones e iteración (`find_first_occurrence` y `safe_get_element`)
### 1. Razonamiento paso a paso y estrategia TDD (RED $\rightarrow$ GREEN $\rightarrow$ REFACTOR)
#### Declaración del contrato:
1. `find_first_occurrence(items: &[u32], target: u32) -> Option<usize>`:
- Debe retornar el índice en base cero de la **primera aparición** de `target`.
- Si no existe o si la colección está vacía, debe retornar `None`.
- Si existen duplicados (por ejemplo, `[4, 7, 7, 9]` y objetivo `7`), debe devolver estrictamente `Some(1)`.
2. `safe_get_element(items: &[u32], index: usize) -> Option<u32>`:
- Debe retornar `Some(valor)` si `index < items.len()`.
- Debe retornar `None` si el índice está fuera de límites, sin provocar pánicos.
#### Ciclo TDD:
1. **Fase RED:** Escribimos las firmas retornando `todo!()` y la suite de pruebas cubriendo primera coincidencia, ausencias, duplicados, listas vacías y límites de acceso por índice. Comprobamos el fallo.
2. **Fase GREEN:**
- Usamos `items.iter().position(|&item| item == target)` para encontrar la primera coincidencia de forma perezosa y con cortocircuito inmediato.
- Usamos `items.get(index).copied()` para leer de forma segura el elemento por índice.
3. **Fase REFACTOR:** Comprobamos que el código es conciso, idiomático y no realiza copias innecesarias.
---
### 2. Implementación canónica completa y ejecutable
// Contract: Returns the zero-based index of the first element equal to target. // Returns None if the target is absent or the slice is empty. // Guarantees that the earliest match is returned in the presence of duplicates. pub fn find_first_occurrence(items: &[u32], target: u32) -> Option
// Contract: Safely accesses an element at the specified index without risking panics. // Returns Some(value) if the index is valid; returns None if out of bounds. pub fn safe_get_element(items: &[u32], index: usize) -> Option
mod tests { use super::{find_first_occurrence, safe_get_element};
fn finds_first_occurrence_and_respects_duplicate_ordering() { let data = [4, 7, 7, 9];
// Duplicate target must yield the first index (index 1, not index 2) assert_eq!(find_first_occurrence(&data, 7), Some(1));
// First physical element assert_eq!(find_first_occurrence(&data, 4), Some(0));
// Last physical element assert_eq!(find_first_occurrence(&data, 9), Some(3));
// Target absent in populated slice assert_eq!(find_first_occurrence(&data, 42), None);
// Target absent in empty slice let empty: [u32; 0] = []; assert_eq!(find_first_occurrence(&empty, 7), None); }
fn accesses_elements_safely_without_panicking() { let data = [10, 20, 30];
// Valid boundary accesses assert_eq!(safe_get_element(&data, 0), Some(10)); assert_eq!(safe_get_element(&data, 2), Some(30));
// Out-of-bounds access returns None safely assert_eq!(safe_get_element(&data, 3), None); assert_eq!(safe_get_element(&data, 100), None); } }
#### Justificación técnica de diseño:
- **`items.iter().position(...)`:** El método `.position()` es un consumidor terminal de cortocircuito optimizado en ensamblador. En cuanto localiza la primera coincidencia, detiene la iteración inmediatamente sin inspeccionar los elementos restantes.
- **`items.get(index).copied()`:** `.get()` devuelve `Option<&u32>`. Dado que `u32` implementa `Copy`, `.copied()` transforma la referencia en el valor escalar poseído `Option<u32>` en cero ciclos adicionales de reloj, liberando el préstamo sobre el slice original.
---
### 3. Respuestas a las preguntas de comprensión profunda (Lección 04)
#### Pregunta 1: La referencia doble en la firma de `.filter()`
- `values.iter()` produce elementos de tipo `&i32`.
- El método `.filter()` requiere un predicado con firma `FnMut(&Self::Item) -> bool`. Al sustituir `Self::Item` (que ya es `&i32`), el argumento que recibe la clausura es `&(&i32)` ($\&\&i32$). Por ello, para comparar el número subyacente con `15` se requieren dos desreferenciaciones (`**x`).
- Si usamos `values.into_iter()`, el iterador consume el vector y rinde valores poseídos `i32` (`Self::Item = i32`). En ese caso, `filter` recibe `&Self::Item`, que es un único puntero `&i32`.
#### Pregunta 2: Los tres modos de iteración y el estado del contenedor
- Con `for name in names.iter()`, la variable `names` solo fue prestada de forma inmutable; sigue siendo perfectamente utilizable después del bucle.
- Con `for name in names.into_iter()`, el vector `names` fue consumido por valor (*move*). Su descriptor en la pila fue desmantelado y ya no puede ser referenciado; intentar usar `names` después del bucle causa el error `E0382`.
- Para modificar elementos en sitio sin crear un nuevo vector, debes usar `names.iter_mut()`. El tipo que recibe el bucle es `&mut String`, lo que permite invocar métodos mutantes (como `name.push_str("...")`).
#### Pregunta 3: Evaluación perezosa y fusión de iteradores
- Con `let pipeline = data.iter().map(...).take(2);`, **no se imprime ningún mensaje en la consola**. Los adaptadores de iterador son puramente perezosos: solo declaran una máquina de estados sin ejecutar ningún ciclo de reloj hasta que un método consumidor lo solicite.
- Si añadimos `.collect::<Vec<_>>()`, la clausura del `map` se ejecuta **únicamente 2 veces**. El adaptador `.take(2)` detiene el consumo tras extraer el segundo elemento, ahorrando por completo el cómputo de los 3 elementos restantes.
#### Pregunta 4: Peligros de indexación directa `v[i]` en arquitecturas web
- Si un atacante envía `selected_index = 999999` sobre un vector de 2 elementos y el código utiliza `cart.items[selected_index]`, la comprobación de límites de Rust falla e invoca un `panic!`. En un servidor web síncrono o mal configurado, esto aborta el hilo de trabajo y puede agotar el pool de conexiones (denegación de servicio, DoS).
- El uso de `cart.items.get(selected_index)` retorna `None` pacíficamente, permitiendo al controlador interceptar la ausencia y devolver de inmediato un código `HTTP 400 Bad Request` o `HTTP 422 Unprocessable Entity` con un mensaje estructurado en JSON.
#### Pregunta 5: La paradoja de hardware: ¿Cuándo $O(N)$ vence a $O(1)$?
- Para una lista pequeña de 6 palabras (`[&str; 6]`), el segundo ingeniero tiene la razón física absoluta.
- **Razón:** Los procesadores transfieren memoria RAM en líneas de caché L1 de 64 bytes. Un arreglo de 6 referencias cabe holgadamente en una o dos líneas de caché contiguas con latencia de acceso de 1 nanosegundo. Escanear 6 palabras secuencialmente toma menos de 10 ciclos de CPU y tiene cero fallos de caché (*cache misses*).
- En contraste, un `HashSet` debe calcular el hash de 64 bits de cada palabra (con SipHash), calcular el índice de cubeta y saltar a direcciones de memoria dispersas en el heap, provocando casi con certeza fallos de caché que tardan entre 50 y 100 nanosegundos cada uno. La búsqueda hash solo supera al arreglo cuando la colección crece a cientos o miles de elementos, donde el costo lineal $O(N)$ se vuelve prohibitivo.
---
### 4. Errores comunes y contraejemplos (Lección 04)
#### Error común: Sobrescribir el índice de coincidencia y devolver el último duplicado
// Código defectuoso: devuelve la última coincidencia en vez de la primera pub fn find_last_instead_of_first(items: &[u32], target: u32) -> Option
*Problema:* Rompe el contrato de dominio que exigía devolver estrictamente la primera coincidencia, además de realizar un recorrido completo del vector innecesariamente.
---
## Verificación de compilación de todas las soluciones
Todas las soluciones de esta guía han sido compiladas y verificadas de forma unificada utilizando el arnés de pruebas de la biblioteca estándar de Rust:
rustc --test solutions_test.rs -o solutions_test.exe ./solutions_test.exe
Salida verificada del arnés de pruebas:
running 4 tests test tests::test_lesson_01_shipping_fee ... ok test tests::test_lesson_02_count_matching ... ok test tests::test_lesson_03_parse_priority ... ok test tests::test_lesson_04_find_first_occurrence_and_safe_get ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s