wandres.dev
CONTROL DE FLUJO · if, loop, while, match

Los bucles: loop, while y for

loop con break que devuelve valor, while, for sobre rangos e iteradores, y las etiquetas de bucle para romper bucles anidados. Por qué Rust prefiere el for sobre iteradores al for de C.

⏱ 13 min

Rust tiene tres bucles, y cada uno responde a una pregunta distinta: loop para repetir hasta que algo lo interrumpa, while para repetir mientras una condición aguante, y for para recorrer los elementos de un iterador. El detalle que sorprende es que loop, fiel a la orientación a expresiones de Rust, puede devolver un valor con break. Y las etiquetas de bucle convierten el control de flujo anidado en algo legible sin recurrir a banderas ni a goto.

🎯 Al terminar esta lección sabrás
  • Usar loop como expresión con break valor.
  • Distinguir while de loop y saber cuándo cada uno.
  • Recorrer rangos e iteradores con for, y ver su desazucarado.
  • Romper y continuar bucles anidados con etiquetas 'outer.

loop: el bucle infinito que devuelve valor

loop repite su cuerpo para siempre, hasta que un break lo detiene. Su rasgo único en Rust: break puede llevar un valor, y ese valor se convierte en el de toda la expresión loop. Es el único bucle que produce algo útil:

let mut intentos = 0;
let resultado = loop {
    intentos += 1;
    if intentos == 10 {
        break intentos * 2; // sale del loop y devuelve 20
    }
};
println!("{resultado}"); // 20

¿Por qué solo loop devuelve valor y no while ni for? Porque loop sin break no termina nunca: el compilador sabe que la única salida es un break, y por tanto cada break es el punto donde nace el valor. Un while o un for pueden ejecutar su cuerpo cero veces, así que no hay ningún valor sensato que devolver; por eso siempre valen ().

while: repetir mientras la condición aguante

while evalúa una condición booleana antes de cada vuelta y para en cuanto es falsa. Es un loop con un if ... break implícito, pero más claro cuando la condición es el corazón de la lógica:

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

Rust no tiene do-while; si necesitas ejecutar el cuerpo al menos una vez, se usa loop con un break condicional al final. Y no hay conversión implícita a booleano: la condición debe ser bool, no un entero. while 1 no compila. Esto elimina de un plumazo la clase entera de bugs del if (x = 0) de C.

// el "do-while" de Rust: loop con el corte al final del cuerpo
loop {
    let linea = leer_linea();
    procesar(&linea);
    if linea.is_empty() { break; } // el cuerpo ya corrió al menos una vez
}

for: recorrer iteradores

El for de Rust no es el for(;;) de C con contador manual, sino un recorrido sobre cualquier cosa iterable. Sobre un rango:

for i in 0..5 {      // 0, 1, 2, 3, 4  (exclusivo al final)
    println!("{i}");
}
for i in 0..=5 {     // 0, 1, 2, 3, 4, 5  (inclusivo con =)
    println!("{i}");
}

Y sobre una colección, prestándola para no consumirla:

let numeros = vec![10, 20, 30];
for n in &numeros {          // n: &i32, presta cada elemento
    println!("{n}");
}
for (i, n) in numeros.iter().enumerate() {
    println!("indice {i} vale {n}");
}

Y como el for solo sabe pedir next(), cualquier adaptador que transforme el iterador sirve de fuente sin tocar el bucle: recorrer al revés, saltar de dos en dos o encadenar rangos es cambiar el iterador, no el for.

for i in (0..10).rev() { /* 9, 8, 7, ... 0 */ }
for i in (0..100).step_by(25) { /* 0, 25, 50, 75 */ }
for c in "ab".chars().chain("cd".chars()) { /* a, b, c, d */ }

Esto no es azúcar superficial. for x in cosa se desazucara a un loop sobre el iterador:

let mut iter = IntoIterator::into_iter(cosa);
loop {
    match iter.next() {
        Some(x) => { /* cuerpo del for */ }
        None => break,
    }
}

El for toma la cosa por IntoIterator, pide elementos con next() —que devuelve Option— y para cuando recibe None. Por eso recorrer con for en lugar de indexar con un contador elimina de raíz los errores de índice fuera de rango (off-by-one) y no paga ningún coste: el iterador se compila al mismo bucle que escribirías a mano.

ℹ️
Los rangos son valores, no sintaxis

0..5 no es una construcción especial del for: es un valor de tipo Range<i32>, y 0..=5 uno de tipo RangeInclusive<i32>. Ambos implementan el trait Iterator, así que puedes guardarlos en variables, pasarlos a funciones y encadenarles adaptadores: (0..100).step_by(2).sum::<i32>(). El for no sabe nada de rangos en particular; solo sabe pedir next(). Esa uniformidad —rangos, vectores, mapas, líneas de un fichero, todos son “algo con next()”— es la que hace el bucle for tan potente.

Etiquetas: romper el bucle correcto

Cuando anidas bucles, break y continue afectan solo al más interno. Para actuar sobre uno exterior, se le pone una etiqueta con comilla simple, y se nombra en el break o continue:

'exterior: for fila in 0..alto {
    for col in 0..ancho {
        if tablero[fila][col] == objetivo {
            println!("hallado en {fila},{col}");
            break 'exterior;    // sale de AMBOS bucles de golpe
        }
        if col == saltar {
            continue 'exterior; // salta a la siguiente fila
        }
    }
}

Sin etiquetas, esto exigiría una bandera booleana (let mut encontrado = false;) comprobada tras el bucle interno: más ruido y más superficie para equivocarse. Las etiquetas expresan la intención directamente. Y se combinan con el valor de break: un break 'etiqueta valor desde un bloque etiquetado —'calc: { ... break 'calc x; }— devuelve ese valor, dándote un “retorno temprano” local sin salir de la función.

flowchart TD
Q[Que necesito repetir] -->|un numero fijo o recorrer algo| F[for sobre rango o iterador]
Q -->|mientras aguante una condicion| W[while]
Q -->|hasta que algo lo rompa y quiza devolver valor| L[loop con break valor]
style F fill:#a6e3a1,color:#11111b
style W fill:#89b4fa,color:#11111b
style L fill:#cba6f7,color:#11111b
El for de Rust es un iterador disfrazado, y eso lo cambia todo

En C, un bucle es un salto condicional y un contador que tú administras: tú decides el índice inicial, tú la condición de parada, tú el incremento, y tú cargas con cada error de frontera que se te escape. Rust hace un movimiento sutil y profundo: el bucle for no itera sobre índices, sino sobre un iterador, un objeto con un único método next() que entrega Some(elemento) o None. Ese desplazamiento —de “controlar un contador” a “consumir una secuencia”— es la puerta a todo el Rust idiomático que viene después. Porque si recorrer es pedir elementos a un iterador, entonces filtrar, mapear, plegar o tomar solo los primeros diez no son construcciones nuevas del lenguaje, sino adaptadores que envuelven un iterador y devuelven otro, componibles como tuberías: datos.iter().filter(|x| x.activo).map(|x| x.id).take(10). Y como todo se monomorfiza e integra en el mismo bucle que escribirías a mano, no pagas nada por la abstracción: es la “abstracción de coste cero” en su forma más pura. El for no es una versión limitada del de C; es la boca de entrada a un modelo de cómputo entero. Aprende a verlo así y habrás dado el primer paso hacia pensar en Rust en lugar de traducir C.

⚔️ Domina los tres bucles
  1. Escribe con loop una raíz cuadrada entera por búsqueda: incrementa un contador hasta que su cuadrado supere n, y devuelve con break el valor anterior.
  2. Traduce ese mismo loop a un while y razona cuál lee mejor y por qué.
  3. Recorre un Vec con for ... in &v y luego con .iter().enumerate(); observa los tipos de la variable ligada en cada caso.
  4. Escribe dos bucles for anidados que busquen un valor en una matriz y usen break 'exterior para salir de ambos; después reescríbelo con una bandera booleana y compara la legibilidad.