ZIG - Slice
In Zig una slice è, in pratica, un puntatore ad una sequenza di valori. Si tratta di un meccansimo leggero ed efficiente ed è parte fondamentale del linguaggio. Formalmente è composta da una coppia puntatore + lunghezza quest'ultima espressa come usize. La differenza sostanziale rispetto ad un array è che per quest'ultimo la lunghezza è nota a compile time e fa parte del tipo stesso, mentre per una slice la lunghezza è nota a runtime. La definizione formale infatti risulta la seguente:
[]T
e come si vede la dimensione non fa parte del tipo diversamente da quanto abbiamo visto negli array dove anzi dimensioni diverse originano tipi array diversi anche a parità di tipi costituivi gli elementi interni. Ancora più formalmente:
[]T ≡ struct {
ptr: [*]T, // pointer-to-many
len: usize, // length
}
vedete quella proprietà len? E' quella informazione che viene presa in carico a runtime, prima non fa parte delle informazioni per così dire compilate, anche se è un po' grossolana come immagine.
Una slice può essere mutabile o immutabile (se definita const)
In altre parole, è una vista su una sequenza di elementi,
non li possiede ma li punta soltanto. La cosa peraltro non è così banale e
bisogna ragionare bene e con attenzione perchè questo è uno dei punti
cardine del linguaggio.
La sequenza può essere un array, un
buffer. Ad esempio possiamo definire appunto un array:
const arr = [_]i32{10,20,30,40}
e su di esso costruiamo il seguente programma:
Esempio 12.1
const std = @import("std");
pub fn main() void {
var arr = [_]i32{ 10, 20, 30, 40 };
const x1: usize = 0;
const slice = arr[x1..arr.len];
std.debug.print("slice: {any}\n", .{slice});
const nome_tipo = @typeName(@TypeOf(slice));
std.debug.print("Il tipo = {s}\n", .{nome_tipo});
}
Il cui output è il seguente:
| slice: { 10, 20, 30, 40 } Il tipo = *[4]i32 |
In particolare la seconda riga ci dice che abbiamo un puntatore ad un array. Questo perchè gli indici sono noti a comptime. Un puntatore ad un array non può essere modificato nella sua lunghezza perchè, come detto nel capitolo relativo agli array, la lunghezza degli array insieme al tipo degli elementi "è" l'array. Abbiamo introdotta una seconda costante chiamandola slice ma non è una slice. Vediamo ora una variazione al programma aggiungendo una riga subito dopo la definizione di x1:
Esempio 12.2
const std = @import("std");
pub fn main() void {
var arr = [_]i32{ 10, 20, 30, 40 };
var x1: usize = 0;
_ = &x1;
const slice = arr[x1..arr.len];
std.debug.print("slice: {any}\n", .{slice});
const nome_tipo = @typeName(@TypeOf(slice));
std.debug.print("Il tipo = {s}\n", .{nome_tipo});
}
var x1: usize = 0;
abbiamo reso x1 variabile
_ = &x1;
questa riga cambia tutto. In pratica usando la variabile di scarto _ diciamo
al compilatore che questa variabile deve essere valutata a runtime in quanto
viene in un certo senso presa in carico da un'altro elemento, che al
compilatore non deve interessare, diciamo così. essendo variabile ed essendo
"puntata" da un altro elemento il compilatore è costretto a valutarlo a
runtime in quanto in pratica abbiamo catturato il suo indirizzo e lo abbiamo
assegnato alla variabile di scarto. In questo modo la costante slice è effettivamente una slice in cui la lunghezza
non è passata come nota a compile time. Infatti l'output dell'esempio 12.2
è:
| slice: { 10, 20, 30, 40 } Il tipo = []i32 |
Un'altra possibilità per generare una vera e propria slice la troviamo nel codice seguente, forse più intuitivo:
Esempio 12.3
const std = @import("std");
pub fn main() void {
const x1: []const i32 = &.{ 1, 2, 3, 4 };
const nome_tipo = @typeName(@TypeOf(x1));
std.debug.print("Il tipo = {s}\n", .{nome_tipo});
}
Ora, la riga critica è ovviamente
const x1: []const i32 = &.{ 1, 2, 3, 4 };
a destra c'è un
array, ovvero un *const[4]i32 a destra chiedi esplicitamente []i32 costante.
Zig permette questa conversione implicita in quanto sicura. Quindi l'output
è il seguente:
| Il tipo = []const i32 |
e anche in questo caso abbiamo una vera e propria slice. La differenza
tra
[ ] i32
e
[ ]const i32
sta ovviamente nel fatto che la seconda è una costante e non può essere modificata e a sua volta &.{1,2,3,4} è di tipo *const [4]i32
Anche le slice sono indicizzate e 0-based, ovvero il loro primo indice è 0.
Esempio 12.4
const std = @import("std");
pub fn main() void {
var arr = [_]i32{ 10, 20, 30, 40, 50 };
var x1: usize = 2;
_ = &x1;
const slice = arr[x1..arr.len];
std.debug.print("Elemento indice 0: {}\n", .{slice[0]});
}
L'output è il numero 30 ovvero l'elemento all'indice 2 dell'array che però corrisponde all'indice 0 per la slice. Anche in questo caso vedete che l'operatore [ ] è quello utilizzato per estrarre il singolo elemento. Nel caso di slice variabili possiamo usare detto operatore anche per modificare gli elemento della slice stessa.
Esempio 12.5
const std = @import("std");
pub fn main() void {
var arr = [_]i32{ 10, 20, 30, 40, 50 };
var x1: usize = 2;
_ = &x1;
var slice = arr[x1..arr.len];
std.debug.print("Elemento indice 0: {}\n", .{slice[0]});
slice[0] = 100;
std.debug.print("Elemento indice 0 dopo modifica: {}\n", .{slice[0]});
}
Anche le slice, ovviamente, essendo indicizzate non possono essere accedute oltre il loro range. In caso di tale errore viene rilevato un panic: index out of bounds: a runtime.
Esiste ancora un dettaglio che ci farà capire la cura che Zig mette nel verificare che non facciamo sbagli grossolani. Prendiamo ad esempio il seguente programma
Esempio 12.6
const std = @import("std");
pub fn main() void {
const array = [_]u8{ 'a', 'b', 'c', 0 }; // elemento indice 3 = 0
// Chiedo una slice dei primi 3 elementi con sentinella 0}
const slice: [:0]const u8 = array[0..3 :0];
const t = @typeName(@TypeOf(slice));
std.debug.print("Tipo della slice: {s}\n", .{t});
}
Fin qui tutto bene, come è facile constatare. Tuttavia se modifichiamo l'array come segue:
const array = [_]u8{ 'a', 'b', 'c',
'd' };
il programma non colpila perchè non è rispettato il valore atteso per la sentinella e il messaggio di errore è molto chiaro:
error: value in memory does not match slice sentinel
Questo è certamente un utile controllo per evitare guai seri.
Le slice le rivedremo all'opera nell'ambito delle stringhe, prossimo paragrafo.