01 · Tipos y flujo de control#
Objetivo#
Al terminar esta lección podrás seguir con precisión matemática el tipo de cada valor en Rust, distinguir una expresión que produce un dato de una instrucción que descarta un resultado, y traducir una regla de dominio a una función pura respaldada por pruebas automatizadas exhaustivas. La pregunta central que te acompañará como ingeniero no es “¿qué sintaxis debo memorizar?”, sino “¿qué estados válidos modela este tipo, cómo fluyen los datos en memoria y qué resultado unificado produce cada rama de ejecución?”.
Antes de empezar#
Necesitas una terminal de comandos y el compilador de Rust instalado (rustc). Esta lección se apoya exclusivamente en las herramientas estándar del lenguaje, sin dependencias externas:
- Para compilar un ejecutable:
rustc main.rs - Para compilar y ejecutar pruebas unitarias integradas:
rustc --test main.rs
Si estás leyendo este material en un entorno donde Rust aún no está instalado, realiza el ejercicio de predicción mental paso a paso y anota tus hipótesis antes de verificar la ejecución; nunca asumas un resultado sin haberlo contrastado contra el compilador. Si encuentras términos desconocidos durante la lectura, consulta el glosario técnico.
Diccionario de conceptos de la lección (Glosario desde cero)#
Para dominar Rust desde los fundamentos, analizamos cada término bajo cuatro dimensiones: intuición cotidiana, definición técnica formal, ejemplo concreto en código y concepción errónea frecuente.
1. Tipo estático (Static Type)#
- Intuición cotidiana: Es como la forma geométrica de una pieza de construcción: un cilindro solo encaja en un orificio redondo. No necesitas encender la máquina para saber si la pieza cabe o no; su forma física lo determina de antemano.
- Definición técnica precisa: Propiedad formal que el compilador asocia a cada valor, variable y expresión durante la fase de análisis semántico. Determina el tamaño en memoria (en bytes), la disposición binaria (layout), los valores válidos y las operaciones permitidas. En Rust, la verificación de tipos ocurre al 100% en tiempo de compilación (compile-time); no hay verificación de tipos en tiempo de ejecución (runtime) ni sobrecoste de rendimiento asociado.
- Ejemplo en código:
let count: u32 = 10;
// let invalid: u32 = "hello"; // Compiler error: expected u32, found &str
- Concepción errónea: Creer que un tipo estático es una simple “etiqueta informativa” o un comentario. En Rust, un tipo es una garantía inviolable de hardware y memoria: el compilador rechaza generar código binario si existe la menor inconsistencia.
2. Tipos escalares (u32, i32, bool, f64)#
- Intuición cotidiana: Son los átomos indivisibles de la información. Un número para contar objetos que no pueden ser negativos (
u32), un indicador de interruptor encendido/apagado (bool), o una medición científica continua (f64). - Definición técnica precisa: Tipos primitivos que representan un único valor en la pila (stack) con tamaño y representación binaria fijos:
u32(unsigned 32-bit integer): Entero sin signo de 32 bits ($4$ bytes). Rango: $0$ a 2³² - 1 (4{,}294{,}967{,}295).i32(signed 32-bit integer): Entero con signo en complemento a dos de 32 bits. Rango: -2³¹ a 2³¹ - 1 (-2{,}147{,}483{,}648 a 2{,}147{,}483{,}647). Tipo entero por defecto en Rust.bool(boolean): Valor lógico de 1 byte de tamaño ($8$ bits), donde0x01estruey0x00esfalse.f64(64-bit floating point): Número en punto flotante de doble precisión según el estándar IEEE 754 ($8$ bytes), con signo, mantisa y exponente.- Ejemplo en código:
let quantity: u32 = 42;
let temperature_delta: i32 = -5;
let is_active: bool = true;
let ratio: f64 = 3.1415926535;
- Concepción errónea: Pensar que
boolocupa solo 1 bit en memoria. Las CPUs modernas direccionan memoria por bytes, por lo que un booleano en Rust ocupa 1 byte completo.
3. Literal#
- Intuición cotidiana: El valor textual exacto que escribes directamente en el papel o en el editor, como escribir el número
42o la palabratrue. - Definición técnica precisa: Notación fija en el código fuente que representa un valor constante de un tipo específico directamente reconocible por el parser del compilador. Rust permite sufijos de tipo explícitos en literales numéricos, como
42_u32o3.14_f64. - Ejemplo en código:
let explicit_u32 = 100_u32;
let explicit_i64 = -100_i64;
let boolean_literal = false;
- Concepción errónea: Creer que
42tiene un tipo fijo predeterminado inmutable antes de la inferencia. Sin anotación, un literal entero entero no restringido se infiere comoi32, pero el compilador puede inferirlo comou8,usize, etc., si el contexto inmediato lo requiere.
4. Binding (let vs let mut)#
- Intuición cotidiana: Una etiqueta identificadora pegada con pegamento industrial a una caja (
let), frente a una etiqueta sobre una caja reutilizable donde puedes cambiar el contenido futuro (let mut). - Definición técnica precisa: Asociación formal entre un identificador (nombre de variable) y una posición de almacenamiento en memoria o valor evaluado. En Rust, todos los bindings creados con
letson inmutables por defecto. La palabra clavelet mutotorga permiso explícito al compilador para permitir que el valor en esa celda de memoria sea modificado posteriormente. - Ejemplo en código:
let base_score = 100;
// base_score = 150; // Error E0384: cannot assign twice to immutable variable
let mut current_score = 100;
current_score = 150; // Válido
- Concepción errónea: Confundir inmutabilidad con constantes (
const). Un binding inmutable conletse calcula dinámicamente en tiempo de ejecución y puede depender de entradas del usuario; una constante (const) debe poder calcularse estrictamente en tiempo de compilación.
5. Expresión (Expression) vs Instrucción (Statement)#
- Intuición cotidiana: Una expresión es una pregunta o fórmula matemática que produce una respuesta (un valor). Una instrucción es una orden ejecutiva que realiza una acción pero no entrega ningún valor como resultado.
- Definición técnica precisa:
- *Expresión (Expression):* Fragmento de código sintáctico que evalúa y produce un valor concreto con un tipo determinado. Ejemplo:
5 + 3evalúa a8_i32;if condition { 1 } else { 2 }evalúa al número correspondiente. - *Instrucción (Statement): Unidad sintáctica que realiza una acción sin retornar un valor de datos. En Rust existen instrucciones de declaración (
let x = 5;) y la instrucción de expresión (expression statement*), que se crea al colocar un punto y coma (;) al final de cualquier expresión, descartando su valor y retornando el tipo unitario(). - Ejemplo en código:
// Expresión: produce 42
let value = {
let step_one = 20;
let step_two = 22;
step_one + step_two // Sin ';' -> retorna 42
};
// Instrucción: descarta el valor
let unit = {
let step_one = 20;
let step_two = 22;
step_one + step_two; // Con ';' -> retorna ()
};
- Concepción errónea: Creer que el punto y coma (
;) es un simple terminador visual obligatorio como en C o Java. En Rust,;es un operador semántico de descarte de valor.
6. Tipo unitario () (Unit Type)#
- Intuición cotidiana: Un sobre vacío o el concepto de "nada que reportar", indicando que una operación concluyó sin producir un dato útil.
- Definición técnica precisa: Tipo en Rust que posee un único valor posible, denotado también como
(). Ocupa exactamente cero bytes en memoria ($0$ bytes de tamaño en compilación y tiempo de ejecución). Es el tipo retornado por defecto por funciones que no especifican->, por bloques terminados en punto y coma, y por instrucciones de efectos secundarios. - Ejemplo en código:
fn log_message(msg: &str) -> () {
println!("{msg}");
}
let result: () = log_message("system initialized");
- Concepción errónea: Confundir
()connullovoid.voiden C/Java significa "ausencia de tipo". En Rust,()es un tipo real, de primera clase, con un valor concreto(), que implementa traits y puede ser pasado o almacenado en colecciones sin ocupar espacio en RAM.
7. Contrato de función (parámetros y retorno ->)#
- Intuición cotidiana: Un contrato legal estricto: "Si me entregas exactamente estos documentos certificados en la ventanilla, te garantizo entregarte a cambio este certificado específico sellado".
- Definición técnica precisa: Firma pública de una función que especifica los tipos obligatorios de los argumentos de entrada y el tipo garantizado del valor de retorno tras la flecha
->. Rust exige que todas las firmas de función declaren sus tipos explícitamente; el compilador no realiza inferencia global de tipos en las firmas de funciones para asegurar estabilidad modular e independencia de compilación. - Ejemplo en código:
fn calculate_tax(amount_cents: u32, rate_per_thousand: u32) -> u32 {
(amount_cents * rate_per_thousand) / 1000
}
- Concepción errónea: Pensar que declarar tipos en los parámetros es redundante. Sin firmas explícitas, cambiar una implementación interna podría cambiar silenciosamente el contrato público y romper consumidores lejanos sin aviso previo.
8. Regla de dominio (Domain Rule)#
- Intuición cotidiana: Las normas del negocio o del mundo real que modelas en el software: por ejemplo, "los envíos son gratuitos si la compra supera los 50 dólares o si el cliente tiene membresía premium".
- Definición técnica precisa: Lógica y restricciones semánticas propias del problema que el software busca resolver. El sistema de tipos de Rust valida que la forma estructural sea coherente (que se entregue un
u32y unbool), pero no puede deducir la intención del negocio. Si escribes||(OR) donde la regla exigía&&(AND), el código compilará sin errores pero violará la regla de dominio. - Ejemplo en código:
// Regla: Solo usuarios verificados con saldo positivo pueden transferir
fn can_transfer(is_verified: bool, balance_cents: u32) -> bool {
is_verified && balance_cents > 0
}
- Concepción errónea: Creer que "si el programa compila, funciona correctamente según el negocio". El compilador verifica la solidez formal y la ausencia de comportamientos indefinidos de memoria, pero las pruebas unitarias son las responsables de verificar las reglas de dominio.
9. Exhaustividad (Exhaustiveness en match)#
- Intuición cotidiana: Un protocolo de seguridad aérea donde cada posible señal de alerta del radar tiene un procedimiento asignado obligatorio; ninguna luz puede quedar sin respuesta prevista.
- Definición técnica precisa: Propiedad impuesta por el compilador de Rust en estructuras de control como
match, la cual exige que el conjunto de ramas (arms) cubra el 100% del espacio de valores admisibles del tipo evaluado. Si existe un solo valor o variante no contemplada, la compilación falla. - Ejemplo en código:
let flag = true;
let status_code = match flag {
true => 200,
false => 403,
// Si comentas cualquiera de las dos ramas, Rust rehúsa compilar
};
- Concepción errónea: Pensar que
matches equivalente a la sentenciaswitchde C, C++ o Java. En esos lenguajes, unswitchsindefaultignora silenciosamente los casos omitidos en tiempo de ejecución. En Rust, omitir un caso es un error fatal de compilación.
10. Atributos de compilación condicional (#[cfg(test)], #[test])#
- Intuición cotidiana: Un interruptor de fábrica que incluye herramientas de diagnóstico en el prototipo de prueba, pero las retira por completo cuando el producto sale a los clientes para no aumentar su peso ni su costo.
- Definición técnica precisa:
#[cfg(test)]: Atributo de metadatos que instruye al compilador a compilar el módulo asociado únicamente cuando se invoca el modo de prueba (rustc --testocargo test). En una compilación normal de producción (rustcestándar), el código anotado es podado (pruned) por el frontend del compilador, reduciendo a cero el tamaño binario y el tiempo de arranque.#[test]: Atributo que marca una función individual como un caso de prueba ejecutable reconocido por el arnés de pruebas de Rust.- Ejemplo en código:
#[cfg(test)]
mod tests {
#[test]
fn basic_verification() {
assert_eq!(2 + 2, 4);
}
}
- Concepción errónea: Creer que las funciones con
#[test]se ejecutan en producción o consumen memoria en el servidor final. Si compilas sin la bandera de prueba, ese código no existe en el binario resultante.
11. Error de compilación E0308 (Mismatched Types)#
- Intuición cotidiana: Intentar pagar en una tienda con una ficha de juego en lugar de una moneda de curso legal; el cajero rechaza la transacción de inmediato porque los objetos no pertenecen a la misma categoría.
- Definición técnica precisa: Código de diagnóstico formal emitido por el compilador de Rust cuando el tipo inferido o proveído para una expresión no coincide con el tipo esperado por el contexto sintáctico (firma de función, binding tipado, o rama alternativa de un
if). - Ejemplo en código:
// let flag: bool = 1; // Error E0308: expected `bool`, found integer
- Concepción errónea: Ver a
E0308como un "obstáculo fastidioso".E0308es una red de seguridad que previene colapsos en producción, conversiones silenciosas no deseadas y desbordamientos de enteros.
Modelo mental y desempacado paso a paso#
1. ¿Por qué Rust prohíbe completamente la coerción implícita de tipos?#
En lenguajes como JavaScript, Python o C, el compilador o runtime realiza conversiones automáticas conocidas como type coercion o truthiness:
- En JavaScript:
if (1)oif ("hello")evalúan a verdadero;1 + "2"produce la cadena"12". - En C: Un entero de 8 bits con signo (
int8_t) puede promoverse silenciosamente a un entero de 32 bits (int32_t), y comparar un entero con signo con uno sin signo (signedvsunsigned) provoca conversiones implícitas que históricamente han causado fallos catastróficos de seguridad (buffer overflows).
Rust aplica una política de cero coerción implícita de tipos. Un número jamás es un booleano; un u8 nunca se promueve automáticamente a un u32. ¿Por qué?
- Diferencias de representación en memoria: Un
u8es un solo byte ($8$ bits); unu32son cuatro bytes ($32$ bits). Promover un valor requiere emitir instrucciones de máquina específicas (comomovzxen x86_64 para extender con ceros). Rust exige que toda transformación física de representación sea explícita mediante la palabra claveaso métodos de conversión (u32::from(byte)). - Eliminación de ambigüedades semánticas: ¿Qué significa
if count? ¿Significacount > 0,count != 0, ocount.is_some()? En Rust debes escribir con total precisión:if count != 0. El compilador obliga al programador a declarar la intención exacta, erradicando una clase entera de errores lógicos.
2. Bloques { ... }, expresiones y el papel del punto y coma (;)#
En Rust, todo bloque delimitado por llaves { ... } es en sí mismo una expresión. El valor al que evalúa el bloque completo es el valor de su última expresión interior:
let score = {
let base = 50;
let bonus = 15;
base + bonus // Última expresión sin ';' -> El bloque evalúa a 65
};
Cuando colocas un punto y coma al final de una línea, transformas esa expresión en una instrucción de expresión (expression statement):
let score = {
let base = 50;
let bonus = 15;
base + bonus; // Con ';' -> Descarta 65 y evalúa a ()
};
Si una función tiene como contrato devolver un booleano (-> bool):
fn is_ready() -> bool {
let status = true;
status; // ¡ERROR E0308!
}
El compilador diagnostica:
error[E0308]: mismatched types
--> src/main.rs:1:18
|
1 | fn is_ready() -> bool {
| -------- ^^^^ expected `bool`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
El ; suprimió el retorno del booleano e hizo que el bloque terminara implícitamente en el tipo unitario ().
3. La estructura de control if como expresión y unificación de tipos#
En Rust, if no es una sentencia de salto de flujo aislada; es una expresión que produce un valor:
let discount = if is_vip { 20 } else { 5 };
Dado que discount debe tener un tipo estático único y determinado en tiempo de compilación, todas las ramas del if deben unificar al mismo tipo:
- Si la rama
trueevalúa au32, la ramaelsedebe evaluar au32. - ¿Qué sucede si escribes un
ifsin ramaelse?
let result = if condition { 100 }; // ¡ERROR E0308!
Un if sin cláusula else asume implícitamente que la rama ausente retorna el tipo unitario (). Por tanto, el compilador compara la rama true ({ 100 }, de tipo entero) con la rama else implícita (()). Al no poder unificar un entero con (), el compilador aborta con el error E0308.
4. Precisión monetaria: ¿Por qué centavos enteros (u32/u64) y no punto flotante (f64)?#
En sistemas de comercio electrónico, facturación o contabilidad, los desarrolladores novatos suelen utilizar números decimales en punto flotante (f64 o float) para representar dinero (por ejemplo, $19.99).
Esto es un grave defecto arquitectónico debido a la naturaleza binaria del hardware moderno bajo el estándar IEEE 754:
- Representación fraccionaria en base 2: En base 10, fracciones como 1/10 ($0.1$) son finitas. Pero en base 2 (binario), $0.1$ es una fracción periódica infinita:
0.000110011001100110011...₂
- Pérdida de precisión acumulada: Debido a que el procesador solo dispone de 53 bits de mantisa en un
f64, el número debe truncarse o redondearse. Si calculas en Rust: ``rust let sum = 0.1_f64 + 0.2_f64; println!("{sum}"); // Imprime: 0.30000000000000004 assert!(sum == 0.3_f64); // ¡PANIC! La aserción falla`` - La solución de dominio: Modela siempre el dinero utilizando la unidad indivisible más pequeña del sistema monetario (por ejemplo, centavos, céntimos o satoshis) representados como enteros sin signo (
u32ou64). $19.99 dólares se representan exactamente como el entero1999_u32. Las operaciones de suma, resta y multiplicación entera son exactas a nivel de registro de CPU y nunca introducen redondeos imprevistos.
Diagrama#
flowchart TD
A[Entrada con tipo conocido] --> B{Condición booleana}
B -->|true| C[Expresión de rama A]
B -->|false| D[Expresión de rama B]
C --> E[Valor con tipo de retorno]
D --> E
El diagrama muestra el flujo de valores, no una garantía sobre la regla de negocio. Un tipo correcto puede contener una decisión semánticamente incorrecta.
Ejemplo razonado#
El siguiente bloque es un programa completo y ejecutable con su suite de pruebas integrada. Puedes guardarlo como main.rs y verificarlo directamente en tu terminal ejecutando rustc --test main.rs:
// Contract: Evaluates whether an actor has permission to edit a resource.
// Only the legitimate owner of the resource can edit it, provided the resource
// has not been marked as archived.
fn can_edit(owner_id: u32, actor_id: u32, archived: bool) -> bool {
owner_id == actor_id && !archived
}
#[cfg(test)]
mod tests {
use super::can_edit;
#[test]
fn only_the_active_owner_can_edit() {
// Case 1: Active owner (owner matches actor, not archived) -> should allow
assert!(can_edit(7, 7, false));
// Case 2: Unauthorized actor (owner does not match actor, not archived) -> should deny
assert!(!can_edit(7, 8, false));
// Case 3: Archived resource (owner matches actor, but archived) -> should deny
assert!(!can_edit(7, 7, true));
}
}
Comentario línea por línea y análisis en profundidad:#
- Líneas 1-3: Documentación del contrato de dominio. Establece la precondición y la regla antes de escribir una sola línea de código ejecutable.
- Línea 4:
fn can_edit(owner_id: u32, actor_id: u32, archived: bool) -> bool owner_id: u32yactor_id: u32: Identificadores únicos numéricos no negativos. Al usaru32, el compilador prohíbe pasar identificadores negativos (como-1), evitando estados inválidos por construcción.archived: bool: Bandera binaria de estado.-> bool: Compromiso inquebrantable de la función de devolver un valor booleano (trueofalse).- Línea 5:
owner_id == actor_id && !archived - La operación
owner_id == actor_idcompara dos enteros de 32 bits a nivel de registros de CPU (cmpen ensamblador) y produce un valor escalarbool. - El operador
&&es un *operador de cortocircuito (short-circuit)*: si la subexpresión izquierda (owner_id == actor_id) evalúa afalse, la CPU no evalúa la subexpresión derecha (!archived), pues el resultado lógico global es necesariamentefalse. - La expresión carece de punto y coma final (
;). Por tanto, el resultado booleano final es evaluado como el valor de retorno de la función. - Línea 8:
#[cfg(test)] - Condiciona la compilación del módulo
tests. Al compilar para producción conrustc main.rs, todo este bloque se omite del binario final. - Línea 12:
#[test] - Registra la función
only_the_active_owner_can_editen el arnés de pruebas de Rust. - Líneas 14-20:
- Caso 1: Verifica el camino feliz (happy path).
- Caso 2: Verifica la frontera de autorización (un actor distinto no debe poder editar).
- Caso 3: Verifica la frontera de ciclo de vida del recurso (un recurso archivado queda congelado contra modificaciones, incluso por su propietario legítimo).
- Observa el uso de
assert!(condicion)para verificar valores verdaderos yassert!(!condicion)para verificar valores falsos.
Práctica independiente#
Ejercicio propuesto: Tarificación de envíos de comercio electrónico#
Debes implementar la función:
pub fn shipping_fee(total_cents: u32, is_member: bool) -> u32
Reglas de dominio especificadas:#
- El umbral para envío gratuito regular es de 5000 centavos ($50.00 dólares).
- Si el total de la orden es estrictamente menor a 5000 centavos, la tarifa estándar de envío es de 500 centavos ($5.00 dólares).
- Si el total de la orden es igual o superior a 5000 centavos, el costo de envío es 0 centavos ($0.00 dólares).
- Si el cliente posee membresía (
is_member == true), el costo de envío es 0 centavos, independientemente del monto total de la compra (incluso en órdenes de 0 centavos).
Matriz de verdad y casos de frontera (Boundary Matrix):#
Antes de escribir la implementación, analiza los valores clave de frontera en la siguiente tabla:
total_cents | Equivalente USD | is_member | Resultado esperado (u32) | Razón / Caso de frontera |
|---|---|---|---|---|
0 | $0.00 | false | 500 | Mínimo valor posible no miembro |
4999 | $49.99 | false | 500 | Justo un centavo debajo del umbral |
5000 | $50.00 | false | 0 | Exactamente en el umbral (gratuito) |
5001 | $50.01 | false | 0 | Justo un centavo sobre el umbral |
0 | $0.00 | true | 0 | Miembro con orden de valor cero |
4999 | $49.99 | true | 0 | Miembro debajo del umbral |
5000 | $50.00 | true | 0 | Miembro en el umbral |
5001 | $50.01 | true | 0 | Miembro sobre el umbral |
Consideraciones de desbordamiento (Overflow):#
- Al usar
u32, el valor máximo admisible es4_294_967_295centavos (más de 42.9 millones de dólares). - La representación en
u32imposibilita quetotal_centssea negativo, protegiendo al sistema contra pedidos fraudulentos con totales negativos.
Flujo de trabajo guiado por pruebas (TDD: RED -> GREEN -> REFACTOR):#
- Fase RED: Crea un archivo
shipping.rs. Declara la firma de la función retornando un valor falso deliberado (comotodo!()o un número fijo999). Escribe el módulo de pruebasmod testscon los 8 casos de la matriz anterior. Ejecuta: ``bash rustc --test shipping.rs`` Comprueba que el compilador compila el arnés pero la prueba falla ruidosamente. Observa el mensaje de fallo. - Fase GREEN: Implementa la lógica mínima necesaria en
shipping_feeutilizando una expresiónif/elseo coincidencia lógica para satisfacer todas las aserciones. Vuelve a ejecutarrustc --test shipping.rshasta obtenertest result: ok.. - Fase REFACTOR: Limpia tu código. ¿Declaraste constantes claras como
const FREE_SHIPPING_THRESHOLD_CENTS: u32 = 5000;para evitar números mágicos (magic numbers)? ¿Es tu expresión limpia y legible? Verifica que las pruebas sigan pasando tras la limpieza.
Preguntas de comprensión profunda y diagnóstico socrático#
Reflexiona sobre estas cinco preguntas de razonamiento antes de avanzar. Cada una explora un límite del modelo mental de Rust:
Pregunta 1: La semántica del punto y coma#
Examina estos dos fragmentos de código:
// Fragmento A
let result_a = {
let a = 10_u32;
a * 2
};
// Fragmento B
let result_b = {
let a = 10_u32;
a * 2;
};
- ¿Qué tipo y qué valor concreto adquiere
result_a? - ¿Qué tipo y qué valor adquiere
result_b? - ¿Por qué los diseñadores del lenguaje Rust eligieron que el operador
;descarte el valor y retorne()en vez de hacer que la última línea de cualquier bloque sea siempre su valor retornado, independientemente de la puntuación?
Pregunta 2: Coerción de verdad (Truthy/Falsy) y solidez de tipos#
En C o JavaScript, el siguiente código es común:
int count = get_pending_tasks();
if (count) {
process_tasks();
}
En Rust, escribir if count { ... } cuando count es u32 provoca un error inmediato de compilación.
- ¿Cuál es el código de error y qué dos tipos entran en conflicto?
- Imagina una pasarela bancaria donde un código numérico
status_codedevuelve0para éxito y números positivos para diversos errores de conexión. Si se permitiera la evaluación implícita booleana al estilo C (donde0esfalse), ¿qué riesgo crítico de seguridad se generaría en el control de flujo?
Pregunta 3: Unificación estática en expresiones condicionales#
Considera este intento de asignación:
let active = true;
let code = if active { 1_u32 };
- Explica por qué el compilador de Rust no permite este código, a pesar de que el programador sabe con certeza que
activeestrue. - ¿Qué tipo le asigna Rust a la ausencia de una cláusula
else? - ¿Cómo se relaciona esto con el principio fundamental de que "toda variable en Rust debe tener un tipo y tamaño exactos conocidos en tiempo de compilación"?
Pregunta 4: Pérdida de precisión en aritmética de punto flotante#
Explica a nivel de bits (según el estándar IEEE 754 de 64 bits):
- ¿Por qué la suma
0.1_f64 + 0.2_f64en un procesador binario no es exactamente representable como0.3_f64? - Supón un sistema contable que factura 100,000 transacciones diarias de micropagos utilizando
f64. ¿Qué consecuencia tiene el redondeo acumulado a fin de mes y por qué el uso de centavos enteros conu32ou64erradica este problema de raíz?
Pregunta 5: Evaluación de cortocircuito y contratos de dominio#
En la función de ejemplo:
fn can_edit(owner_id: u32, actor_id: u32, archived: bool) -> bool {
owner_id == actor_id && !archived
}
- ¿En qué orden exacto evalúa el hardware las subexpresiones?
- Si
owner_id == actor_idresulta serfalse, ¿se evalúa!archived? - Si la segunda condición fuera una llamada a una función con costo de cómputo elevado, como
is_resource_locked_in_database(resource_id), ¿por qué ubicar primero la comprobación barata en memoria (owner_id == actor_id) optimiza drásticamente el rendimiento del sistema?
Code smell#
Síntoma: Lógica condicional duplicada y dispersa en capas de llamada#
Supón que una aplicación necesita verificar si un usuario puede editar un documento. En cuatro lugares diferentes del código fuente (en el controlador HTTP, en el servicio de auditoría, en el exportador a PDF y en el disparador de notificaciones), encuentras repetida la misma expresión booleana:
if document.owner_id == session.user_id && !document.is_archived && !document.is_deleted {
// ...
}
- Evidencia observable: Duplicación textual de múltiples condiciones lógicas booleanas acopladas directamente a campos internos de estructuras.
- Hipótesis del problema: La regla de negocio ("qué condiciones definen la capacidad de edición") carece de un único punto de verdad. Si el negocio cambia y ahora se exige que los usuarios suspendidos tampoco puedan editar, el desarrollador tendrá que localizar y actualizar los cuatro sitios manualmente. La probabilidad de omitir uno es altísima, generando una brecha de seguridad.
- Consecuencia concreta: Un usuario suspendido es rechazado por el controlador HTTP, pero el exportador a PDF le permite generar y descargar el documento archivado.
- Tratamiento mínimo justificado: Extraer la condición a una función pura de dominio con un nombre autoexplicativo (
can_edit(...)) respaldada por pruebas unitarias de casos de borde. - Cuándo NO refactorizar: No extraigas una función si la condición es una comparación trivial de un solo término que se utiliza una única vez en todo el sistema (
if items.is_empty()). Crear abstracciones prematuras para condiciones obvias añade saltos visuales en el código sin mitigar ningún riesgo real de inconsistencia.
Contraejemplo deliberado: Incompatibilidad de tipos en ramas de un if (E0308)#
El siguiente programa es deliberadamente erróneo para que observes el diagnóstico emitido por el compilador. Guárdalo y compílalo con rustc main.rs:
fn main() {
let allowed = true;
let result = if allowed { 1_u32 } else { false };
println!("{result}");
}
El compilador de Rust detecta que la rama if produce un entero escalar u32, mientras que la rama else produce un escalar bool. Al violar la regla de unificación estática de tipos, emite exactamente:
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`
| |
| expected because of this
Criterio de salida#
Puedes considerar superada esta lección con maestría cuando:
- Puedes explicar con tus propias palabras la diferencia entre expresión e instrucción, y por qué colocar un punto y coma (
;) convierte una expresión en el tipo unitario(). - Tu batería de pruebas unitarias para
shipping_feecubre de forma demostrable los valores clave de frontera (mínimo, justo debajo del umbral, en el umbral y justo encima), tanto para miembros como para no miembros. - Puedes argumentar técnicamente por qué modelar montos de dinero con números flotantes (
f64) genera inconsistencias contables y cómo los enteros sin signo (u32) previenen este problema. - Has ejecutado
rustc --testen tu terminal y visto pasar todas tus pruebas contest result: ok..
Lecturas oficiales#
- Rust Book, tipos de datos: Contrasta los tipos escalares y compuestos, y analiza qué garantiza la verificación estática en compilación.
- Rust Book, flujo de control: Profundiza en las expresiones
if, bucles y unificación de ramas. - Rust By Example: Ejemplos sintácticos concisos para experimentar rápidamente.
- Glosario técnico del curso: Consulta definiciones canónicas de términos avanzados y referencias cruzadas.
Siguiente paso#
Continúa con 02 · Ownership y préstamos, donde descubrirás cómo el compilador de Rust gestiona la memoria en la pila y el montículo sin necesidad de un recolector de basura (garbage collector), mediante reglas de pertenencia y préstamos verificadas en tiempo de compilación.