While
Para construir un bucle usando while usamos la siguiente estructura:
while (condición booleana){
// Código ejecutado de manera cíclica mientras se cumpla la condición
}
El bloque de código entre llaves se ejecutará una y otra vez, siempre y cuando la condición booleana sea verdadera:
- Si la condición no se cumple desde el inicio, entonces el bucle no se ejecutará ni una sola vez. Es como si se tratara de un if.
- Si, en un momento, durante la ejecución, la condición deja de cumplirse, el bucle no será ejecutado otra vez, ya que la condición se evalúa otra vez antes de empezar a ejecutar el bloque.
- Si la condición se cumple siempre, el bucle será infinito.
El bucle while es muy usado, cuando el número de iteraciones es desconocido, sin embargo si se conoce el número de iteraciones, se suele usar una variable contador que se va incrementando (o decrementando) en cada iteración, para controlar la condición de salida:
|
// contador para el índice var n_i: usize = 0; // iteramos mientras n_i es menor o igual a 10 while (n_i <= 10) { // ejecutamos instrucciones y vamos incrementando n_i n_i += 1; |
No es la única manera de controlar un bucle while, pero es muy común hacerlo así. En Zig, para no tener que escribir este incremento dentro del bloque, podemos ponerlo así de forma más compacta:
|
var n_i: usize = 0; // iteramos mientras n_i es menor o igual a 10 while (n_i <= 10):(n_i += 1) { // ejecutamos instrucciones pero ya no incrementamos n_i dentro del bloque |
En Zig, es posible, pero no obligatorio, ampliar un while con un bloque else:
while (condición booleana) : (incremento opcional) {
// Código ejecutado de manera cíclica mientras se cumpla la condición
} else {
// Código ejecutado sólo una vez cuando no se cumple la condición
}
El bloque else de un while se ejecuta siempre y una sola vez, ya sea:
- a la salida del while
- si la condición booleana no llega a ser verdadera nunca.
Eso sí - podemos interrumpir el bucle while en cualquier momento usando la palabra clave break. Esto hará que el bucle se detenga de inmediato, sin siquiera pasar por el bloque else (si existe).
Y si lo que queremos es saltar directamente al inicio del bucle, ignorando el resto del bloque podemos usar la palabra clave continue.
while (condición booleana){
// para saltar inmediatamente al inicio del bucle usamos continue
// para salir inmediatamente del bucle usamos break
} else {
// ejecutado sólo una vez cuando no se cumple la condición y si no usamos break
}
En el siguiente ejemplo el jugador está buscando en las taquillas del laboratorio todas las pociones curativas que pueden ayudarle a reparar el daño infligido por el ataque de sus enemigos.
Sabe que:
- A partir de la taquilla número 4 solo hay salsas y comida
- Alguien ha colocado una salsa barbacoa en la segunda taquilla por error.
Solo vamos a listar aquellas pociones que de verdad tienen efectos curativos:
while.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { // diferentes pociones mágicas const a_lockers = [_] // no especificamos el número exacto []const u8{ // nombres como textos de longitud fija "járabe de la abuela", // 0 "salsa barbacoa", // 1 "limonada", // 2 "tabasco", // 3 "mayonesa", // 4 "ketchup", // 5 // ... }; print("Las pociones curativas en las taquillas:\n", .{}); // contador para el índice var n_i: usize = 0; // repasando el inventario en busca de las pociones curativas: while (n_i < a_lockers.len) : (n_i += 1) { // recuperamos la poción en el índice const s_potion = a_lockers[n_i]; // si es salsa barbacoa no se cuenta if (n_i == 1) continue; // si es tabasco salimos del inventario // después del tabasco no hay nada que no sea comida if (n_i == 3) break; print("\tTaquilla {}: {s}\n", .{ n_i + 1, s_potion }); } } |
|
$ zig run while.zig |
|
Las pociones curativas en las taquillas: Taquilla 1: jarabe de la abuela Taquilla 3: limonada |
En el ejemplo:
- continue ignora la salsa barbacoa sin imprimirla,
- break sale del bucle a partir de la posición del tabasco,
- Solo imprimimos las posiciones que pueden ser útiles para curar.
While infinito
Antes, cuando hablamos de funciones recursivas, vimos que si una función se llama a sí misma demasiadas veces sin detenerse, provoca que se rompa el programa: con cada llamada se come un poco de la memoria reservada y al final se agota. Como vimos, este gasto de memoria sucede aunque no se le pase ningún parámetro en las llamadas.
Y ¿Qué sucede con un while que no acaba nunca?
while_infinite.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { print("antes del while\n", .{}); while (true) { // nada aquí de momento } print("Nunca llega aquí\n", .{}); } |
|
~$ zig run while_infinite.zig |
|
antes del while |
Nada. Literalmente no pasa nada. Un while infinito, por sí solo no consume más recursos en cada iteración - aquí gira en el vacío a la espera de ser interrumpido de alguna manera. Para detenerlo podemos pulsar las teclas Ctrl y c a la vez (Ctrl+c) - así se envía una señal de interrupción al programa desde la terminal.
Obviamente, si dentro del bucle tenemos código que consume recursos sin liberarlos - como asignar memoria, abrir archivos sin cerrarlos etc. puedes colapsar el programa. Pero por sí mismo un while no lo hará..
While con captura de valores opcionales
En Zig existe un concepto que mencionamos justo ahora porque combina muy bien con los bucles while: la captura de valores opcionales.
Un valor opcional en Zig puede tener un valor concreto o ser null - es decir nada, no tener nada. Por lo tanto su tipo tampoco se corresponde con el tipo de valores que esperamos manejar. Es como un contenedor vacío. ¿Por qué y cómo necesitamos gestionar esto?
El jugador llega a una habitación y encuentra unas cajas misteriosas que contienen energía pura. Algunas de estas cajas, por desgracia, contienen el vacío absoluto - su valor es literalmente null. Cuando el jugador abre una caja puede encontrarse un número positivo - la energía almacenada o… nada - pero ese nada no es un 0. No es un numéro ni ningún valor… y ¿qué sucede?
while_unwrap_null.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_boxes = [_]u8{ 100, 10, null, // caja vacía 30, 120, null, // caja vacía }; var n_i: usize = 0; while (a_boxes[n_i]) |n_pts| : (n_i += 1) { print("{}. ¡Has ganado {} puntos de energía!\n", .{ n_i + 1, n_pts }); } else { print("{}. ¡Caja vacía! \n", .{n_i + 1}); } } |
|
$ zig run while_unwrap_null.zig |
|
while_unwrap_null.zig:8:9: error: expected type 'u8', found '@TypeOf(null)' null, // caja vacía ^~~~ |
El programa ha “explotado”. Zig espera que los elementos del array a_boxes fueran del tipo u8 pero de repente el jugador “abre” la caja y el programa se encuentra un null. Eso no es un número. No es nada y por eso falla en tiempo de compilación.
Para poder gestionar un dato que podría ser null tenemos que definir estos elementos indicando su tipo con un signo ? delante de su tipo habitual. En este caso será ?u8:
while_unwrap_null_fix.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_boxes = [_]?u8{ // <- el tipo opcional indicado 100, 10, null, // caja vacía 30, 120, null, // caja vacía }; var n_i: usize = 0; while (a_boxes[n_i]) |n_pts| : (n_i += 1) { print("{}. ¡Has ganado {} puntos de energía!\n", .{ n_i + 1, n_pts }); } else { print("{}. ¡Caja vacía! \n", .{n_i + 1}); } } |
|
$ zig run while_unwrap_null_fix.zig |
|
1. ¡Has ganado 100 puntos de energía! 2. ¡Has ganado 10 puntos de energía! 3. ¡Caja vacía! |
El programa ya no se rompe como antes. Sin embargo, como el while con captura se detiene cuando el valor resulta ser un null todo se para en la tercera caja. No está tan mal pero se puede mejorar. El jugador quiere poder abrir todas las cajas. ¿Qué podemos hacer para recorrer y procesar todos los elementos?
Esta es una buena ocasión para aprovechar lo que ya sabemos hacer con switch: el salto a una etiqueta, y combinarlo con lo que hemos aprendido sobre los while. Sí, esto es complicarlo un poco, luego vemos una manera más sencilla de hacerlo, pero usar continue :etiqueta valor es bastante divertido ¿no crees?
while_unwrap.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_boxes = [_]?u8{ // tipo opcional 100, 10, null, // caja vacía 30, 120, null,// caja vacía }; var n_i: usize = 0; loop: switch (n_i) { else => { while (a_boxes[n_i]) |n_pts| : (n_i += 1) { print("{}. ¡Has ganado {} puntos de energía!\n", .{ n_i + 1, n_pts }); } else { print("{}. ¡Caja vacía! \n", .{n_i + 1}); n_i += 1; if (n_i < a_boxes.len) continue :loop n_i; } }, } } |
|
$ zig run while_unwrap.zig |
|
1. ¡Has ganado 100 puntos de energía! 2. ¡Has ganado 10 puntos de energía! 3. ¡Caja vacía! 4. ¡Has ganado 30 puntos de energía! 5. ¡Has ganado 120 puntos de energía! 6. ¡Caja vacía! |
Ahora el jugador abre todas las cajas y el programa es capaz de gestionar tanto los valores con el tipo esperado como los null.
Saltar a etiquetas, me parece muy divertido, lo admito. Pero la mayoría de las veces hay una manera más simple. Si alguna vez sientes la tentación (como me pasa a mí), piensa que posiblemente hay una solución sin saltos… aunque sea más aburrida:
while_unwrap_2.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_boxes = [_]?u8{ 100, 10, null, 30, 120, null, }; var n_i: usize = 0; while (n_i < a_boxes.len) : (n_i += 1) { if (a_boxes[n_i]) |n_pts| print("{}. ¡Has ganado {} puntos de energía!\n", .{ n_i + 1, n_pts }) else print("{}. ¡Caja vacía! \n", .{n_i + 1}); } } |
|
$ zig run while_unwrap_2.zig |
|
1. ¡Has ganado 100 puntos de energía! 2. ¡Has ganado 10 puntos de energía! 3. ¡Caja vacía! 4. ¡Has ganado 30 puntos de energía! 5. ¡Has ganado 120 puntos de energía! 6. ¡Caja vacía! |