ZIG - il tipo union
nb: capitolo in aggiornamento - non è tutto chiaro
In Zig, union è un tipo che permette di memorizzare uno solo tra più tipi
possibili nello stesso spazio di memoria. E' una sorta di
contenitore dall'interno del quale possiamo utilizzare un oggetto solo per
volta. Ricorda il tipo union del C, ma con
alcune differenze importanti. Distingueremo tra union senza e con tag. La
cosa è abbastanza differente in particolare gli union senza tag possono
esporre a pericoli che il linguaggio invece di solito evita.
Come
potrete intuire la keyword che identifica questo tipo è
union.
Partiamo quindi dal classico
esempio che trovate ovunque si parli di questo argomento:
const Valore = union {
intero: i64,
float: f64,
booleano: bool,
};
Qui abbiamo una unione che fa contenitore per 3 elementi di diversa natura. I vari campi interni ha un identificativo un tipo sono separati e chiusi con una virgola. Iniziamo a sottolineare, che rispetto ad una struct abbiamo un vantaggio in termini di occupazione di memoria. Lo vediamo qui di seguito
Definiamo una struct: occupa sempre 16 byte (somma di tutti i campi:
const SenzaUnion = struct {
intero: i64, // 8 byte
float: f64, // 8 byte
};
una analoga union: occupa solo 8 byte (il campo più grande)
const ConUnion = union {
intero: i64, // 8
byte
float: f64, // 8 byte <- stessa memoria!
};
Detto questo andiamo a sviluppare il codice precedente:
Esempio 17.1
const std = @import("std");
pub fn main() void {
const Valore = union {
intero: i64,
float: f64,
booleano: bool,
};
var v1 = Valore{ .intero = 42 };
v1.intero = 3;
std.debug.print("Valore v1: {}\n", .{v1.intero});
}
questo codice compila e gira tranquillamente. Ma cosa succederebbe aggiungessimo la seguente riga, prima della stampa:
v1.float = 1.5;
a runtime otterreste questo:
thread 13224 panic: access of union field 'float' while field 'intero' is active
Il numero del thread può cambiare, anzi sicuramente cambierà, ma l'errore no. Come detto un variabile di tipo unione può "accendere" solo uno dei suoi campi. Quindi il compilatore non sa chi è l'elemento attivo. Possiamo anche scrivere questo codice:
var v1 = Valore{ .intero = 42 };
v1.intero = 3;
std.debug.print("Valore v1: {}\n", .{v1.intero});
v1 = Valore{
.float = 3.14 };
std.debug.print("Valore v1: {}\n", .{v1.float});
e allora tutto funziona ma se l'ultima istruzione, quella di stampa del float la riscriviamo ancora come come:
std.debug.print("Valore v1: {}\n", .{v1.intero});
otteniamo di nuovo un errore simile a quello precedente a runtime. Uno per volta, è la regola.
thread 2820 panic: access of union field 'intero' while field 'float' is active
Questo tipo di unione viene usato molto raramente. Ovviamente
se definiamo noi le variabili che di volta in volta usiamo
non ci sarebbero problemi perchè lì siamo noi a tenere traccia delle
variabili in uso. Le criticità sorgono quando siamo costretti a
lavorare con una sola variabile che può essere manipolata in varie
parti del codice ad esempio può essere riassegnata o manipolata da
varie funzioni.
Per questo motivo il tipo union è quasi sempre
usato congiunzione con un enumeratore che tiene traccia del campo
attivo. Si parla allora di tagged union e
sarà quello che useremo nella grande maggioranza dei casi
Darò solo
un esempio ripromettendomi di approfondire successivamente il
discorso.
Esempio 17.2
const std = @import("std");
// Tagged union: il tipo viaggia insieme al valore
const Valore = union(enum) {
intero: i64,
float: f64,
booleano: bool,
};
fn stampa(v: Valore) void {
// Il compilatore sa esattamente cosa sta dentro
// e mi OBBLIGA a gestire tutti i casi
switch (v) {
.intero => |n| std.debug.print("(tagged) intero: {}\n", .{n}),
.float => |f| std.debug.print("(tagged) float: {}\n", .{f}),
.booleano => |b| std.debug.print("(tagged) booleano: {}\n", .{b}),
}
}
pub fn main() void {
std.debug.print("---\n", .{});
// --- Tagged union: il tipo viaggia col valore ---
var x = Valore{ .intero = 42 };
stampa(x); // stampa correttamente "intero: 42"
x = Valore{ .float = 3.14 };
stampa(x); // stampa correttamente "float: 3.14"
}
La collaborazione con switch è evidente ed essenziale.
Come ultima osservazione, è importante evidenziare che anche il tipo union può includere delle funzioni. Il concetto è sempre quello dei namespace che inglobano un funzione nel loro spazio niente di diverso anche concettualmente da quanto abbiamo già per struct ed enumerativi. Anche qui darò un esempio basico:
Esempio 17.3
const std = @import("std");
const U = union(enum) {
a: i32,
b: f32,
pub fn print(self: U) void {
switch (self) {
.a => |x| std.debug.print("a = {}\n", .{x}),
.b => |x| std.debug.print("b = {d}\n", .{x}),
}
}
};
pub fn main() void {
var xu1: U = .{ .a = 10 };
var xu2: U = .{ .b = 3.14 };
xu1.print();
xu2.print();
}