Selezione - switch


Come abbiamo detto nel paragrafo relativo all'istruzione if, avere una ramificazione eccessiva, ovvero troppi else + if non è cosa buona che rende lettura e manutenzione del programma non particolarmente agevole. Come in altri linguaggi, esiste una istruzione più chiara che nasce apposta per casi come questi ed è introdotta dalla keyword switch. Ricorda da vicino l'omologa istruzione del C ma prevede anche la sicurezza dell'istruzione match usata in Rust. Anche qui useremo la keyword else e fa la sua comparsa l'operatore composto =>.
La struttura standard è la seguente:

switch (variabile o costante) {
  valore-1 => istruzioni,
  valore-2 => istruzioni,
  ......
  valore-n => istruzioni,
  },
  else => istruzioni,
}

in cui le istruzioni possono essere una o più. In questo linguaggio non è possibile il fall-through ovvero la ricaduta in rami successivi dopo che se ne è utilizzato uno precedente. In molti linguaggi è necessario usare una istruzione tipo break (C, C++, C#, per dirne qualcuno). La dimenticanza di tale istruzione può creare grossi problemi. Questa situazione in Zig non si presenta e una volta che viene eseguito un ramo di codice gli altri vengono saltati automaticamente.  Vediamo un esempio:

Esempio 8.1
const std = @import("std");
pub fn main() void {
  const x1 = 10;
  switch (x1) {
    10 => {
      std.debug.print("x1 vale 10\n", .{});
      std.debug.print("x1 + 5 = {}\n", .{x1 + 5});
      },
    11 => std.debug.print("x1 is 11\n", .{}),
    else => std.debug.print("x1 is not 10\n", .{}),
  }
}

Come si vede, valutiamo il valore di x1 e, a seconda di esso, valorizziamo il ramo corrispondente. Nel contempo vediamo anche il caso in cui ci siano una o più istruzioni.
Una importante osservazione che è doveroso fare, è che la copertura dei casi deve essere completa. Al contrario di if che può contemplare solo alcuni casi con switch non ci possono essere valori non considerati. Ad esempio se togliete else, che fa da rastrello nell'esempio 8.1 di tutti i valori che non siano 10 o 11 ne otterrete:

error: switch must handle all possibilities

più chiaro di così....

le domande standard a questo punto sono almeno due:
1) come si imposta la cosa se devo gestire due o più valori nello stesso ramo?
2) come si imposta la cosa se devo gestire un range ampio di valori sequenziali?
Vediamo le risposte:

1) modifichiamo la parte di valutazione dell'esempio 8.1 in modo che il primo branch prenda in esame 3 valori:
 switch (x1) {
  10, 12, 15 => std.debug.print("x1 vale 10\n", .{}),
  11 => std.debug.print("x1 is 11\n", .{}),
  else => std.debug.print("x1 ha altri valori", .{}),
}

2) come prima stavolta nel secondo ramo inseriamo un range (dei range riparleremo)
 switch (x1) {
  10, 12, 15 => std.debug.print("x1 vale 10\n", .{}),
  0...9 => std.debug.print("x1 minore di 10\n", .{}),
  else => std.debug.print("x1 ha altri valori", .{}),
}

Anche switch è una espressione ed il suo valore di uscita può essere attribuito ad una variabile:

Esempio 8.2
const std = @import("std");
pub fn main() void {
  const x1 = 11;
  const x2 = switch (x1) {
    10 => 100,
    else => 0,
  };
  std.debug.print("x2 vale {}\n", .{x2});
}

attribuendo in questo caso il valore 0 a x2. I tipi dei valori in uscita devono essere uguali.

La completezza richiesta nella copertura dei casi, il blocco forzato dei fall-through, la chiarezza nella gestione dei vari sono i punti di forza di switch in questo linguaggio.