study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

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:
RUST
  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), donde 0x01 es true y 0x00 es false.
  • 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:
RUST
  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 bool ocupa 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 42 o la palabra true.
  • 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_u32 o 3.14_f64.
  • Ejemplo en código:
RUST
  let explicit_u32 = 100_u32;
  let explicit_i64 = -100_i64;
  let boolean_literal = false;
  • Concepción errónea: Creer que 42 tiene un tipo fijo predeterminado inmutable antes de la inferencia. Sin anotación, un literal entero entero no restringido se infiere como i32, pero el compilador puede inferirlo como u8, 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 let son inmutables por defecto. La palabra clave let mut otorga permiso explícito al compilador para permitir que el valor en esa celda de memoria sea modificado posteriormente.
  • Ejemplo en código:
RUST
  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 con let se 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 + 3 evalúa a 8_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:
RUST
  // 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:
RUST
  fn log_message(msg: &str) -> () {
      println!("{msg}");
  }
  let result: () = log_message("system initialized");
  • Concepción errónea: Confundir () con null o void. void en 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:
RUST
  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 u32 y un bool), 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:
RUST
  // 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:
RUST
  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 match es equivalente a la sentencia switch de C, C++ o Java. En esos lenguajes, un switch sin default ignora 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 --test o cargo test). En una compilación normal de producción (rustc está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:
RUST
  #[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:
RUST
  // let flag: bool = 1; // Error E0308: expected `bool`, found integer
  • Concepción errónea: Ver a E0308 como un "obstáculo fastidioso". E0308 es 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) o if ("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 (signed vs unsigned) 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é?

  1. Diferencias de representación en memoria: Un u8 es un solo byte ($8$ bits); un u32 son cuatro bytes ($32$ bits). Promover un valor requiere emitir instrucciones de máquina específicas (como movzx en x86_64 para extender con ceros). Rust exige que toda transformación física de representación sea explícita mediante la palabra clave as o métodos de conversión (u32::from(byte)).
  2. Eliminación de ambigüedades semánticas: ¿Qué significa if count? ¿Significa count > 0, count != 0, o count.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:

RUST
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):

RUST
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):

RUST
fn is_ready() -> bool {
    let status = true;
    status; // ¡ERROR E0308!
}

El compilador diagnostica:

TEXT
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:

RUST
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 true evalúa a u32, la rama else debe evaluar a u32.
  • ¿Qué sucede si escribes un if sin rama else?
RUST
  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:

  1. 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...₂
  2. 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 ``
  3. 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 (u32 o u64). $19.99 dólares se representan exactamente como el entero 1999_u32. Las operaciones de suma, resta y multiplicación entera son exactas a nivel de registro de CPU y nunca introducen redondeos imprevistos.

Diagrama#

DIAGRAMA MERMAID
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

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

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:

RUST
// 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: u32 y actor_id: u32: Identificadores únicos numéricos no negativos. Al usar u32, 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 (true o false).
  • Línea 5: owner_id == actor_id && !archived
  • La operación owner_id == actor_id compara dos enteros de 32 bits a nivel de registros de CPU (cmp en ensamblador) y produce un valor escalar bool.
  • El operador && es un *operador de cortocircuito (short-circuit)*: si la subexpresión izquierda (owner_id == actor_id) evalúa a false, la CPU no evalúa la subexpresión derecha (!archived), pues el resultado lógico global es necesariamente false.
  • 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 con rustc main.rs, todo este bloque se omite del binario final.
  • Línea 12: #[test]
  • Registra la función only_the_active_owner_can_edit en 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 y assert!(!condicion) para verificar valores falsos.

Práctica independiente#

Ejercicio propuesto: Tarificación de envíos de comercio electrónico#

Debes implementar la función:

RUST
pub fn shipping_fee(total_cents: u32, is_member: bool) -> u32

Reglas de dominio especificadas:#

  1. El umbral para envío gratuito regular es de 5000 centavos ($50.00 dólares).
  2. 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).
  3. 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).
  4. 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_centsEquivalente USDis_memberResultado esperado (u32)Razón / Caso de frontera
0$0.00false500Mínimo valor posible no miembro
4999$49.99false500Justo un centavo debajo del umbral
5000$50.00false0Exactamente en el umbral (gratuito)
5001$50.01false0Justo un centavo sobre el umbral
0$0.00true0Miembro con orden de valor cero
4999$49.99true0Miembro debajo del umbral
5000$50.00true0Miembro en el umbral
5001$50.01true0Miembro sobre el umbral

Consideraciones de desbordamiento (Overflow):#

  • Al usar u32, el valor máximo admisible es 4_294_967_295 centavos (más de 42.9 millones de dólares).
  • La representación en u32 imposibilita que total_cents sea negativo, protegiendo al sistema contra pedidos fraudulentos con totales negativos.

Flujo de trabajo guiado por pruebas (TDD: RED -> GREEN -> REFACTOR):#

  1. Fase RED: Crea un archivo shipping.rs. Declara la firma de la función retornando un valor falso deliberado (como todo!() o un número fijo 999). Escribe el módulo de pruebas mod tests con 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.
  2. Fase GREEN: Implementa la lógica mínima necesaria en shipping_fee utilizando una expresión if/else o coincidencia lógica para satisfacer todas las aserciones. Vuelve a ejecutar rustc --test shipping.rs hasta obtener test result: ok..
  3. 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:

RUST
// 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:

C
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_code devuelve 0 para éxito y números positivos para diversos errores de conexión. Si se permitiera la evaluación implícita booleana al estilo C (donde 0 es false), ¿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:

RUST
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 active es true.
  • ¿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_f64 en un procesador binario no es exactamente representable como 0.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 con u32 o u64 erradica este problema de raíz?

Pregunta 5: Evaluación de cortocircuito y contratos de dominio#

En la función de ejemplo:

RUST
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_id resulta ser false, ¿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:

RUST
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:

RUST
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:

TEXT
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_fee cubre 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 --test en tu terminal y visto pasar todas tus pruebas con test result: ok..

Lecturas oficiales#


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.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)