ZIG - Gli array
In Zig, come in altri linguaggi, gli array sono una sequenza indicizzata, fissa e finita di elementi dello stesso tipo. Il numero di elementi può andare da 0 (array vuoto) a n e gli indici sono di tipo usize e partono da 0 per arrivare ad n-1. In Zig, la lunghezza degli array deve essere nota a compile-time (poiché fa parte del tipo), ma il loro contenuto può essere mutabile e determinato a runtime.
Definizione classica ed esaustiva. Sul punto relativo alla presenza di
elementi dello stesso tipo all'interno di un array, tuttavia,
ci sarà qualche precisazione da fare. Lo vedremo in chiusura di questo capitolo.
Dal punto di vista formale un array si
definisce molto semplicemente come segue:
[dimensione] tipo
La dimensione può essere un u32, un i32, un comptime_int, insomma quello che volete purchè sia valutabile in compilazione. Detto questo, come sempre in Zig, bisogna provvedere alla inizializzazione. Abbiamo diverse strade:
const arr: [5]i32 = .{ 1, 2, 3, 4, 5 };
abbiamo definito l'array definendo il suo nome, specificando la lunghezza e il tipo a sinistra del segno uguale. A destra abbiamo invece un array initializer con il tipo che viene ricavato per inferenza. E' equivalente a questa scrittura:const arr: [5]i32 = [5]i32{1,2,3,4,5};
const arr = [_]i32{ 1, 2, 3, 4, 5 };
in forma più semplice con il compilatore che deduce la lunghezza in base al numero degli elementi passati nell'inizializzazione.
const arr = [_]i32{0} ** 5;
con valore ripetuto tramite l'operatore di ripetizione **. Questo dà origine ad un array di 5 elementi tutti uguali a 0 (in questo caso, se no scegliete voi un valore).
var arr: [5]i32 = undefined;
arr = .{ 1, 2, 3, 4, 5 };
std.debug.print("Array: {any}\n", .{arr});
In questo caso abbiamo un array con elementi non inizializzati che poi, prima dell'uso, vengono successivamente definiti.
1 const std = @import("std");
2 pub fn main() !void {
3 const arr = init: {
4 var tmp: [26]u8 = undefined;
5 for ('a'..'z') |c| tmp[@intCast(c - 'a')] = @intCast(c);
6 break :init tmp;
7 };
8 std.debug.print("Array: {any}\n", .{arr});
9 }
Questa è una forma di inizializzazione interessante che merita il codice completo. In pratica ricorriamo ad una label, una etichetta alla riga 3 che chiamiamo init dalla quale facciamo iniziare un blocco di codice, che termina alla riga7 all'interno del quale definiamo un array che al termine tramite break attribuiamo all'array arr. In realtà è un esempio che mostra come lavorare con questi blocchi che somigliano come logica alle funzioni anonime di altri linguaggi. La versione semplificata di inizializzazione tramite for è la seguente:var arr: [26]u8 = undefined;
for ('a'..'z') |c| arr[@intCast(c - 'a')] = @intCast(c);
Direi che questi metodi per ora possono bastare, anche se il discorso non sarebbe chiuso ma avremo tempo per pensare a cosa più fantasiose. E' interessante notare però come, una volta che abbiate creato due array sia immediato crearne un terzo che risulti essere la somma dei due: si fa uso dell'operatore ++:
const arr1 = [_]i32{ 1, 2, 3, 4, 5 };
const arr2 = [_]i32{ 6, 7,
8, 9, 0 };
const arr3 = arr1 ++ arr2;E' il
momento di vedere come si lavora con gli array.
La prima cosa e più
semplice, è verificarne la lunghezza, ovvero il numeri di elementi. Si usa
una proprietà dal nome descrittivo: len
const arr = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("Array:
lunghezza = {any}\n", .{arr.len});
ne risulterà il numero 5, come gli elementi dell'array.
Un altro problema che si presenta di solito è quello di iterare tra gli elementi. Ci sono vari sistemi, un modo molto semplice lo abbiamo usando for:
Esempio 11.1
const std = @import("std");
pub fn main() !void {
var x1: i32 = 0;
const arr = [_]i32{ 1, 2, 3, 4, 5 };
for (arr) |item| {
std.debug.print("{}\n", .{item});
x1 = x1 + item;
}
std.debug.print("Somma elementi: {}\n", .{x1});
}
che stampa i 5 elementi dell'array nonchè la loro somma.
Un altro
modo fa uso dell'operatore [] con il quale
possiamo individuare il singolo elemento nell'array.
const arr = [_]i32{ 1, 2, 3, 4, 5 };
for (0..arr.len) |i|
{
std.debug.print("{}\n", .{arr[i]});
}
Il citato operatore quindi serve anche per estrarre un singolo elemento:
const arr = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("{}\n",
.{arr[3]});
Se indicate un elemento fuori range il compilatore se ne accorge. Se invece l'indice viene indicato a runtime e non è nel range corretto succede un mezzo disastro (che dovremo imparare a gestire, si capisce). A questo proposito vediamo il seguente esempio che sarà in buona parte spiegato in dettaglio in apposito paragrafo. Per adesso ci interessa capire la parte finale, dove viene gestito l'indice inserito dall'utente:
Esempio 11.2
const std = @import("std");
pub fn main() !void {
try std.fs.File.stdout().writeAll("Inserisci un numero: ");
var stdin_buf: [256]u8 = undefined;
var stdin_reader = std.fs.File.stdin().reader(&stdin_buf);
const stdin = &stdin_reader.interface;
const line = try stdin.takeDelimiterExclusive('\n');
const trimmed_line = std.mem.trimRight(u8, line, "\r");
const value = try std.fmt.parseInt(u32, trimmed_line, 10);
const arr = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("arr = {any}\n", .{arr[value]});
}
La riga di nostro interesse è sostanzialmente l'ultima, graffa finale a parte. Viene visualizzato l'elemento che si trova all'indice indicato dall'utente. Se l'utente inserisce un numero che va da 0 a 4 andiamo bene, altrimenti il programma va in crash in modo fragoroso. Fate le vostre prove. Questo per dire che il compilatore vi accompagna fin dove può ma non è onnipotente.
Gli array non sono immutabili e possiamo, se non modificarne la dimensioni, che quelle non si possono cambiare, sostituirne gli elementi costitutivi. Ovviamente non si possono modificare array definiti const ma solo quelli var.
Esempio 11.3
const std = @import("std");
pub fn main() !void {
var arr = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("arr = {any}\n", .{arr});
arr[0] = 10;
std.debug.print("arr = {any}\n", .{arr});
}
da cui otteniamo:
| arr = { 1, 2, 3, 4, 5 } arr = { 10, 2, 3, 4, 5 } |
con sostituzione dell'elemento all'indice 0.
Una cosa interessante può essere la ricerca di un elemento all'interno di un array. Ci sono vari metodi, alcuni, lo avrete intuito, che potete costruire da voi con for, if e break ad esempio. Un modo idiomatico però è il seguente che fa uso di indexOfScalar: come vediamo nel seguente frammento di codice:
const arr = [_]i32{ 1, 2, 3, 4, 5 };
const pos =
std.mem.indexOfScalar(i32, &arr, 3);
std.debug.print("{any}\n", .{pos});
indexOfScalar richiede come primo parametro il tipo in uscita, poi un puntatore all'array (dei puntatori riparleremo) e infine l'elemento cercato. Se lo trova ne restituisce l'indice, diversamente avremo il valore null.
E' facilmente possibile creare array multidimensionali:
Esempio 11.4
const std = @import("std");
pub fn main() void {
const m = [_][3]i32{
[_]i32{ 1, 2, 3 },
[_]i32{ 4, 5, 6 },
};
std.debug.print("{any}\n", .{m});
}
Da notare che la prima dimensione, quella degli array componenti può essere inferita, la seconda no, il compilatore deve sapere con che tipi ha a che fare e poichè la dimensione fa parte del tipo, ovvero [N1]T è diverso da [N2]T questo parametro deve essere specificato. Insomma solo la dimensione esterna può essere inferita, quella interna no.
Una ulteriore modalità di rappresentazione è quella prevede una sentinella. Cosa significa, semplicemente che al termine, come ultimo elemento, piazziamo un carattere particolare che funge da fine dell'array. Questo è qualche cosa che ci ricorda abbastanza da vicino quanto presente nel linguaggio C. In questo caso, la definizione formale dell'array cambia leggermente:
[N:x]T
che alloca N+1 elementi di tipo T ove l'ultimo, x, funge da limite estremo, ovvero da sentinella.
Esempio 11.5
const std = @import("std");
pub fn main() void {
const arr = [_:9]u8{ 1, 2, 3, 4, 5 };
std.debug.print("Array: {any}\n", .{arr});
std.debug.print("Elementi: {d}\n", .{arr.len});
std.debug.print("Sentinella: {d}\n", .{arr[5]});
}
con questo output:
| Array: { 1, 2, 3, 4, 5 } Elementi: 5 Sentinella: 9 |
La sentinella evidentemente non fa parte della "lunghezza" del nostro array.
Se volete effettuare la copia di un array la cosa è molto semplice: si usa l'operatore = come nel seguente esempio che evidenzierà anche un aspetto importante:
Esempio 11.6
const std = @import("std");
pub fn main() void {
var x1 = [_]i32{ 10, 20, 30, 40 };
var x2 = x1;
x1[0] = 100;
x2[1] = 200;
std.debug.print("x1: {any}\n", .{x1});
std.debug.print("x2: {any}\n", .{x2});
}
che ci presenta il seguente output:
| x1: { 100, 20, 30, 40 } x2: { 10, 200, 30, 40 } |
da cui si evidenzia che i due array, x1 e x2, sono indipendenti l'uno dall'altro, ovvero siamo di fronte ad una deep copy.
Chiudiamo con una considerazione riprendendo quanto detto all'inizio. Avevamo specificato che un array deve essere costituito da elementi dello stesso tipo. Questo è vero in generale ma ci può essere qualcosa che differisce leggermente da quanto detto, sia pure solo in apparenza. Ad esempio:
Esempio 11.7
const std = @import("std");
pub fn main() void {
const arr1 = [_]i32{ 1, 2, 3, 4.0, 5 };
const arr2 = [_]f32{ 1.0, 2, 3, 4.0, 5.0 };
const arr3 = [_]i32{ 1.0, 2, 3, 4.4, 5.0 };
std.debug.print("Array1: {any}\n", .{arr1});
std.debug.print("Array2: {any}\n", .{arr2});
std.debug.print("Array3: {any}\n", .{arr3});
}
Questo codice non compila e l'errore è determinato da arr3.
L'array
arr1 comprende sia numeri interi che un numero con virgola ma essendo questo
privo di parte decimale significativa, 4.0 è riportato nell'ambito degli i32
senza perdita di informazione.
L'array arr2 comprende un intero in mezzo
agli f32 ma anche in questo caso la coercizione è possibile da i32 ad f32
senza perdita di informazione
L'array arr3 invece prevede 4.4 in mezzo
agli interi e la conversione ad i32 comporterebbe perdita di precisione. Per
questo il compilatore non ci sta e lo segnala:
error: fractional
component prevents float value '4.4' from coercion to type 'i32
Quindi è vero che gli elementi di un array devono essere dello stesso tipo ma questa uniformità può essere imposta tramite coercizione laddove possibile.
Il discorso possiamo chiuderlo qui per ora ma le maniere per manipolare gli array sono davvero tante. Qui ho dato solo una infarinatura di base al fine di permettere di lavorare con un minimo di elasticità. In futuro mi riprometto di ampliare ulteriormente il materiale.