Switch
Switch es una estructura de control que hace dos cosas: permite ejecutar un bloque de código y también permite devolver un valor.
Un switch, se compone de ramas (casos) que tienen asociados sus bloques de código. De esta manera, cada caso define qué código se ejecuta si la variable tiene un cierto valor.
switch (variable){
valor1 => {
// el código contenido aquí se ejecuta sólo si la variable es igual al valor1
},
valor2 => {
// se ejecuta sólo si la variable es igual al valor2
},
…
valorA, valorB, valorC => {
// se ejecuta sólo si la variable es igual a uno de los valores: valorA, valorB,...
},
…
valorN => {
// se ejecuta sólo si la variable es igual al valorN
},
…
Rango de valores => {
// se ejecuta sólo si la variable está dentro del Rango de valores
},
…
else {
// se ejecuta sólo si la variable no pertenece a ninguno de los casos anteriores
}
}
Switch tiene que cubrir todos los valores posibles del tipo de dato evaluado.
Si no defines todos los casos de forma explícita o al menos no incluyes una rama else, Zig nos avisará de ello con un error:
switch_incomplete.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age = 16; switch (n_age) { 0...3 => { print("Eres un bebé\n", .{}); }, 4...10 => { print("Eres pequeñ@\n", .{}); }, 11...15 => { print("Eres muy joven\n", .{}); }, 16...17 => { print("Joven pero ya puedes conducir\n", .{}); }, } } |
|
$ zig run switch_incomplete.zig |
zig run switch_incomplete.zig
|
switch_incomplete.zig:6:5: error: switch must handle all possibilities switch (n_age) { ^~~~~~ |
En el switch anterior hemos usado rangos de valores definidos con valor_inicio … valor_fin. En este caso podemos optar por una de las dos soluciones posibles:
- definir un tipo para n_age y evaluar el rango para el máximo valor de ese tipo,
- o añadir un bloque else para cubrir todos los números posibles
switch_incomplete_fix_1.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age: u8 = 16; switch (n_age) { 0...3 => { print("Eres un bebé\n", .{}); }, 4...10 => { print("Eres pequeñ@\n", .{}); }, 11...15 => { print("Eres muy joven\n", .{}); }, 16...17 => { print("Joven pero ya puedes conducir\n", .{}); }, 18...255 => { print("Mayor de edad", .{}); }, } } |
|
$ zig run switch_incomplete_fix_1.zig |
|
Joven pero ya puedes conducir |
En la solución el tipo de n_age lo hemos definido como u8 (enteros positivos de 8 bits), porque si no definimos ningún tipo Zig va a inferir (adivinar si te acuerdas del primer Capítulo) el tipo como comptime_int. Ese tipo es como decir “enteros con valores infinitos”. Infinito es solo un decir- no existe tal cosa en ninguna máquina por muy potente que sea pero como no le hemos dicho el tipo que vamos a usar Zig nos da un poco de “manga ancha” y “piensa” que podría ser infinito aunque realmente no exista tal cosa y además no se puede definir el rango superior de un número Infinitamente grande. Por lo tanto en este y en muchos otros casos es mejor definir los tipos de antemano tal y como hemos hecho y eso nos permite acotar el caso para arreglar nuestro switch.
También podemos poner un simple else:
switch_incomplete_fix_2.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_age = 16; switch (n_age) { 0...3 => { print("Eres un bebé\n", .{}); }, 4...10 => { print("Eres pequeñ@\n", .{}); }, 11...15 => { print("Eres muy joven\n", .{}); }, 16...17 => { print("Joven pero ya puedes conducir\n", .{}); }, else => { print("Mayor de edad", .{}); }, } } |
|
$ zig run switch_incomplete_fix_2.zig |
|
Joven pero ya puedes conducir |
Devolver valores con Switch
Si lo que queremos es asignar el valor desde un switch, en vez de solo ejecutar un bloque de código para un caso concreto, Zig permite hacerlo. Esto es útil cuando queremos almacenar ese valor para utilizarlo después en el resto del programa.
const (o var) identificador = switch (variable){
valor1 => devuelve un valor desde aquí cuando la variable es igual al valor1 ,
valor2 => devuelve un valor desde aquí cuando la variable es igual al valor2 ,
…
valorN => devuelve un valor desde aquí cuando la variable es igual al valorN ,
…
Rango de valores => devuelve desde aquí cuando la variable está dentro del rango
,
…
else => devuelve desde aquí si la variable no coincide con los casos anteriores
}
Igual que antes, switch también tiene que cubrir todos los casos posibles para la variable y además todos los casos tienen que devolver el mismo tipo de valor para que se pueda asignar correctamente al identificador.
En el siguiente ejemplo queremos comprobar si existe una página web. Al acceder a la dirección por http, hemos obtenido como respuesta un código y lo almacenamos en la constante n_code. Con el switch traducimos el código obtenido en un mensaje descriptivo que después imprimimos:
switch_http.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_code = 404; const s_response = switch (n_code) { 200 => "Página existe", 301 => "Redirección", 404 => "Página no encontrada", 500 => "Error de servidor", else => "Error desconocido", }; print("La dirección ha respondido con: {d} '{s}'.\n", .{ n_code, s_response }); } |
|
$ zig run switch_http.zig |
|
La dirección ha respondido con: 404 'Página no encontrada'. |
Captura del valor en una rama
Si queremos recuperar el valor (payload) que se ha evaluado en una rama del switch, lo hacemos con esta sintaxis:
switch (variable){
valor1 => |val1| val1 es capturado cuando la variable es igual al valor1 ,
valor2 => |val2| val2 es capturado cuando la cuando la variable es igual al valor2 ,
…
valA, valB, valC => |val| val es capturado si coincide con alguno de estos valores,
…
valorN => |valN| capturado cuando la variable es igual al valorN ,
…
Rango de valores => |val del rango| capturado cuando la variable está dentro del rango
,
…
else => |val desconocido| cuando la variable no coincide con los casos anteriores
}
Vamos a ver un ejemplo para comprender el posible uso de esta técnica. Imaginemos que queremos imprimir el código HTTP de respuesta, con una descripción:
switch_http_capture.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_code = 404; const t_resp = switch (n_code) { 200...399 => |n_c| .{ n_c, "Página existe o hay redirección" }, 400...499 => |n_c| .{ n_c, "Página no encontrada o la petición no es válida" }, 500...599 => |n_c| .{ n_c, "Error del servidor" }, else => |n_c| .{ n_c, "Error desconocido" }, }; print("La dirección ha respondido con: {d} - '{s}'.\n", t_resp); } |
|
$ zig run switch_http_capture.zig |
|
La dirección ha respondido con: 404 - 'Página no encontrada o la petición no es válida'. |
En el ejemplo anterior devolvemos una tupla t_resp formada por el número de código capturado y una cadena. Funcionalmente, ese ejemplo no tiene mucho sentido, ya que conocemos n_code de antemano. Lo que interesa de ese código es comprender cómo se captura el valor en una rama cuando usamos rangos. A continuación, te muestro una versión más interesante, donde el valor capturado se usa para algo útil.
Switch con break y continue
switch_http_capture_2.zig |
|
const std = @import("std"); const print = std.debug.print; // simular llamada a una página y obtener el código fn get_dummy_page_response() u16 { return 404; } // simular que anotamos las páginas que no existen fn dummy_db_pre_hook() void { print("Apuntar en la base de datos que la página NO EXISTE\n", .{}); } pub fn main() void { // simulamos que obtenemos el código de respuesta de una url const t_resp = sw: switch (get_dummy_page_response()) { // la respuesta 0 es imposible // pero la usamos para una operación interna 0 => { dummy_db_pre_hook(); break :sw .{ 404, "Página no encontrada" }; }, 100, 101, 102 => |n_c| .{ n_c, "Ese código no existe" }, 200...399 => |n_c| blk: { break :blk if (n_c == 200) .{ n_c, "La página existe" } else .{ n_c, "Existe pero hay redirección" }; }, 400...499 => |n_c| blk: { break :blk switch (n_c) { 400 => .{ n_c, "La petición no es válida" }, 401 => .{ n_c, "No autorizado" }, 403 => .{ n_c, "Prohibido" }, 404 => { // volvemos al switch pasando el valor 0 continue :sw 0; }, else => .{ n_c, "Error del cliente" }, }; }, 500...599 => |n_c| .{ n_c, "Error interno del servidor" }, else => |n_c| .{ n_c, "Código de respuesta desconocido" }, }; print("La dirección ha respondido con: {d} - '{s}'.\n", t_resp); } |
|
$ zig run switch_http_capture_2.zig |
|
Apuntar en la base de datos que la página NO EXISTE La dirección ha respondido con: 404 - 'Página no encontrada'. |
En este ejemplo, el código de respuesta ya no está definido como una constante, sino que lo obtenemos simulando una llamada mediante una función. Evaluamos así una expresión dentro del switch.
La captura del valor con |n_c| es necesaria cuando usamos rangos, para poder trabajar inmediatamente después con el valor concreto por el cual se ha entrado en esa rama.
Además, gracias a los bloques etiquetados que ya vimos antes, hemos podido devolver valores obtenidos con switch e if adicionales y así tener mucha más precisión: saber de qué error se trata en el caso de los errores de 400 a 404, por ejemplo. Esto ya nos da mucho más control sobre lo que queremos devolver.
Habrás notado que la etiqueta se llama blk: en los dos bloques. Esto es muy común en Zig. Las etiquetas no tienen un significado especial, más allá del que tú quieres imaginar. Como ya dijimos en el apartado anterior: si no están anidadas - no estás obligado a ponerles diferentes nombres a las etiquetas. El nombre blk: que verás con cierta frecuencia en los ejemplos de Zig, realmente no significa nada especial (“bloque” probablemente). Es solo una convención informal.
También hemos etiquetado el switch con la etiqueta sw: (podría tener otro nombre que se te ocurra poner).
¿Para qué hacemos esto? Es algo muy potente: nos da la posibilidad de volver al inicio del switch después de entrar en una de sus ramas y además pasándole un nuevo valor para evaluar:
|
404 => { // volvemos al switch pasando el valor 0 continue :sw 0; }, |
Esto hace que el switch vuelva a evaluarse pero con el valor 0. En nuestro caso, el código para el caso 0 ejecuta lo siguiente:
|
0 => { // escribimos en la base de datos dummy_db_pre_hook(); break :sw .{ 404, "Página no encontrada" }; }, |
Sí, en este caso podríamos haber llamado a dummy_db_pre_hook() directamente en el caso 404. Lo interesante del ejemplo es que veas cómo podemos saltar de una rama a otra distinta.