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.
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.
- Usar
loopcomo expresión conbreak valor. - Distinguir
whiledeloopy 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.
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
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.
- Escribe con
loopuna raíz cuadrada entera por búsqueda: incrementa un contador hasta que su cuadrado superen, y devuelve conbreakel valor anterior. - Traduce ese mismo
loopa unwhiley razona cuál lee mejor y por qué. - Recorre un
Vecconfor ... in &vy luego con.iter().enumerate(); observa los tipos de la variable ligada en cada caso. - Escribe dos bucles
foranidados que busquen un valor en una matriz y usenbreak 'exteriorpara salir de ambos; después reescríbelo con una bandera booleana y compara la legibilidad.