Control de flujo en Rust: if, match y bucles — Capítulo 7

Expresiones condicionales y estructuras de iteración


En el contexto de un lenguaje de programación como Rust, el control de flujo constituye un pilar fundamental para la estructuración lógica del código. Este capítulo examina las construcciones básicas para condicionales y bucles, esenciales para manejar decisiones y repeticiones de manera segura y predecible. Al dominar estos mecanismos, se facilita la escritura de programas robustos que responden a condiciones variables y procesan datos iterativamente, preparando el terreno para conceptos más avanzados en la gestión de estado y algoritmos.

La sentencia if y sus variantes

La sentencia if en Rust permite la ejecución condicional de bloques de código basada en la evaluación de una expresión booleana. Su sintaxis básica se define como sigue:

if condición {
    // bloque de código si la condición es verdadera
} else {
    // bloque de código si la condición es falsa
}

Aquí, condición debe ser una expresión que evalúe a un valor de tipo bool. A diferencia de lenguajes como C o JavaScript, donde se permite la coerción implícita de tipos a booleanos, Rust exige una expresión estrictamente booleana, lo que previene errores comunes derivados de conversiones inesperadas. Por ejemplo, un entero no cero no se interpreta automáticamente como verdadero; en su lugar, se requiere una comparación explícita, como if x != 0 { ... }.

La cláusula else es opcional, y se pueden encadenar múltiples condiciones mediante else if para formar estructuras de decisión más complejas. Un ejemplo ilustrativo es el siguiente fragmento:

let numero = 42;

if numero < 0 {
    println!("Número negativo");
} else if numero == 0 {
    println!("Cero");
} else {
    println!("Número positivo");
}

En este caso, el flujo se ramifica secuencialmente hasta encontrar la primera condición verdadera, ignorando las subsiguientes. Es importante destacar que los bloques deben estar delimitados por llaves {}, incluso para una sola sentencia, lo que refuerza la legibilidad y evita ambigüedades. Un caso borde surge cuando la condición es una constante booleana: if true { ... } ejecuta siempre el bloque verdadero, mientras que if false { ... } lo omite por completo.

Rust trata if no solo como una sentencia, sino también como una expresión cuando se utiliza para asignar valores. En este modo, cada rama debe producir un valor del mismo tipo, y la expresión if evalúa al valor de la rama ejecutada. Por instancia:

let resultado = if numero > 0 {
    "positivo"
} else {
    "no positivo"
};

Esta forma expresiva es particularmente útil en asignaciones concisas, aunque requiere que todas las ramas terminen con una expresión sin punto y coma para devolver un valor. Si se omite la cláusula else, la rama implícita devuelve (), el tipo unitario, lo que limita su uso a contextos donde se espera este tipo. Comparado con lenguajes como Python, donde las condicionales ternarias son sintaxis separada, Rust integra esta funcionalidad directamente en if, promoviendo un código más unificado.

Un detalle sutil radica en el ámbito de las variables declaradas dentro de los bloques: estas no escapan al bloque if, alineándose con el principio de ownership de Rust. Por ejemplo, una variable declarada en la rama if no está disponible en la rama else ni fuera de la estructura.

La expresión match

La expresión match representa una de las herramientas más potentes y versátiles para el control de flujo en Rust, permitiendo la coincidencia de patrones contra un valor y la ejecución de código asociado. Su sintaxis general se estructura como:

match valor {
    patrón1 => expresión1,
    patrón2 => expresión2,
    // ... 
    _ => expresión_por_defecto,
}

Cada brazo consiste en un patrón seguido de => y una expresión, separada por comas. El valor se compara secuencialmente con cada patrón hasta encontrar una coincidencia, ejecutando la expresión correspondiente. La inclusión de un brazo comodín _ es obligatoria si no se cubren todos los casos posibles, asegurando exhaustividad y previniendo errores en tiempo de compilación. Esto contrasta con construcciones como switch en C++, donde la omisión de casos puede llevar a fallos silenciosos.

match es inherentemente una expresión, lo que significa que evalúa a un valor, requiriendo que todas las expresiones en los brazos produzcan el mismo tipo. Un ejemplo mínimo demuestra su uso con enumeraciones simples:

enum Moneda {
    Euro,
    Dolar,
}

let moneda = Moneda::Euro;

let valor = match moneda {
    Moneda::Euro => 1.0,
    Moneda::Dolar => 1.1,
};

Aquí, valor se asigna según el variante de la enumeración. Los patrones pueden ser más complejos, incluyendo destructuración, como en match punto { Point { x, y } => ... }, o guardas con if para condiciones adicionales: patrón if condición => .... Un caso borde involucra tipos no exhaustivos, como enteros, donde el comodín _ captura todos los valores no especificados.

En comparación con lenguajes funcionales como Haskell, match en Rust enfatiza la seguridad mediante chequeos estáticos, rechazando en compilación cualquier match no exhaustivo. Además, los brazos no caen a través (no hay “fallthrough” como en C), lo que elimina una fuente común de bugs. Si se desea un comportamiento similar, se debe explicitar con múltiples patrones separados por |, como 1 | 2 => ....

El bucle loop

El bucle loop ofrece una forma de iteración indefinida en Rust, ejecutando un bloque de código repetidamente hasta que se invoca una sentencia de salida explícita. Su sintaxis es sencilla:

loop {
    // código a repetir
}

Sin mecanismos de terminación, este bucle continuaría indefinidamente, por lo que comúnmente se combina con break para salir. break puede opcionalmente devolver un valor, convirtiendo loop en una expresión. Por ejemplo:

let mut contador = 0;
let resultado = loop {
    contador += 1;
    if contador == 10 {
        break contador * 2;
    }
};

En este fragmento, resultado recibe el valor devuelto por break, que debe ser consistente en tipo si hay múltiples puntos de salida. Esto difiere de bucles infinitos en lenguajes como Java (while(true) { ... }), donde no hay soporte nativo para valores de retorno, requiriendo variables externas.

continue permite saltar al inicio del bucle, omitiendo el resto del cuerpo en la iteración actual. Un detalle sutil es el manejo de etiquetas para bucles anidados: se puede etiquetar un loop con 'etiqueta: loop { ... } y usar break 'etiqueta o continue 'etiqueta para controlar bucles exteriores. Esto es esencial en escenarios anidados para evitar ambigüedades. Caso borde: un loop vacío con break inmediato evalúa a () si no se especifica valor.

El bucle while

El bucle while ejecuta un bloque mientras una condición booleana se evalúe a verdadero, con sintaxis:

while condición {
    // código
}

Al igual que en ifcondición debe ser estrictamente bool, sin coerción. Un ejemplo básico ilustra su uso para conteo descendente:

let mut numero = 3;
while numero != 0 {
    println!("{}", numero);
    numero -= 1;
}

while no es una expresión y no devuelve valores directamente, a diferencia de loop. Se puede simular con loop y una guarda if !condición { break; }, pero while ofrece claridad semántica. En comparación con C, Rust evita errores como asignaciones en la condición (while (x = 0)) mediante su tipado estricto.

continue y break funcionan de manera similar, con soporte para etiquetas en anidamientos. Un caso borde ocurre cuando la condición es falsa inicialmente, resultando en cero iteraciones. Otro detalle: variables mutables en la condición deben ser actualizadas dentro del cuerpo para evitar bucles infinitos.

El bucle for

El bucle for en Rust se centra en la iteración sobre colecciones mediante iteradores, con sintaxis:

for patrón in iterable {
  // código
}

Aquí, iterable debe implementar el trait IntoIterator, como rangos o vectores. Un ejemplo con un rango:

for i in 1..4 {
    println!("{}", i);
}

Esto itera sobre 1, 2 y 3, excluyendo el límite superior. Para inclusión, se usa 1..=3for desestructura el patrón en cada elemento, facilitando accesos directos. En contraste con bucles for indexados en C++, Rust promueve iteradores para mayor seguridad, evitando accesos fuera de límites.

break y continue son aplicables, con etiquetas para anidamientos. No hay soporte directo para índices automáticos; se debe usar enumerate() para obtener pares (índice, valor). Caso borde: iterar sobre un iterable vacío resulta en cero ejecuciones.

Con estos mecanismos de control de flujo dominados, el siguiente capítulo profundizará en las estructuras de datos fundamentales de Rust, como vectores y mapas, que a menudo se combinan con bucles para procesamientos eficientes.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio