Rust - Il tipo Union


Forse non il più usato nel linguaggio, anzi, possiamo definirlo davvero "di nicchia", il tipo union è anche lui in qualche modo simile ad una struct per alcuni versi e, tra le varie definizioni disponibili mi sento di abbracciare la seguente: è un costrutto che permette di definire una struttura dati in cui diversi tipi di dati possono occupare la stessa posizione in memoria. O meglio, un union è simile alla struct, ma tutti i campi condividono la stessa area di memoria. È simile alle unioni in C e viene utilizzato principalmente proprio per l’interoperabilità con il codice C o per situazioni in cui si vuole gestire manualmente la rappresentazione dei dati in memoria. In effetti si dice spesso che il caso d'uso principale è l'interoperabilità con C tramite FFI (Foreign Function Interface). In C le union sono comuni, e Rust deve poterle rappresentare fedelmente.
Questo tipo, come facile capire, definito dall'utente, quindi permette di sovrapporre più dati nello stesso spazio di memoria e pertanto la sua dimensione è data dall'elemento più grande. Questa convivenza non è esente da rischi tanto che il codice che manipola variabili di tipo unione è marcato con la keyword unsafe, che dice tutto e che ci deve far pensare ad un argomento piuttosto avanzato, quale in realtà è. In conclusione, deve essere manipolato con attenzione ed evidentemente solo in situazioni particolari.
La keyword che definisce questo tipo è union e la sintassi è la seguente:

union Identificatore {
  campo-1: tipo,
  campo-2: tipo,
  ....,
  campo-n: tipo,
}

Vediamo un esempio che dimostra anche uno dei rischi che si corrono con questo particolare tipo:

Esempio 17.1

union MyUnion {
    f1: u32,
    f2: f32,
}

fn main() {
    let mut u = MyUnion { f1: 1 };
    unsafe {
    // Accesso sicuro - sappiamo che `f1` e' stato inizializzato
    println!("f1: {}", u.f1);
    // Modifica del valore tramite `f1` e lettura tramite `f2`
    u.f1 = 123456789;
    println!("f2: {}", u.f2); // Risultato imprevedibile e potenzialmente pericoloso
    }
}

In sostanza nell’esempio, dopo aver inizializzato il campo f1 della union MyUnion, il codice modifica il valore di f1 e poi tenta di leggere f2. Poiché f1 e f2 condividono la stessa posizione in memoria, leggere f2 dopo aver scritto f1 significa interpretare erroneamente i bit di un u32 come se fossero un f32. Questo può portare a risultati imprevedibili e potenzialmente pericolosi, poiché il valore interpretato potrebbe non avere senso come f32 o potrebbe addirittura causare comportamenti indefiniti nel programma. Il compilatore in pratica non sa, all'interno di questo "contenitore", quale membro sia attivo e utilizzabile. Pertanto grande responsabilità è messa sulle spalle del programmatore usando il tipo union e quella parolina, unsafe, già citata ad inizio paragrafo e che è necessaria per accedere ai membri di una unione, lo certifica. L'output del programma ad esempio in una delle mie esecuzioni è:

f1: 1
f2: 0.00000000000000000000000000000000016535997

con interpretazione random dei bit dell'intero come f32. Non bello.

Un'altra limitazione per gli union è che non tutti i tipi sono ammessi. I campi devono implementare Copy, oppure bisogna usare ManuallyDrop<T> per tipi che hanno un distruttore, in pratica non potete inserire una stringa che implementa Drop in quanto, da quel che ho capito, il compilatore non saprebbe come deallocare elementi del genere all'interno di quella "scatola". Non ho investigato questa possibilità.

Quali sono i punti di forza delle unioni? Sostanzialmente, oltre alla possibilità di cooperazione con il C e le tante librerie scritte in quel linguaggio, direi l'estrema efficienza. Le union in Rust sono estremamente efficienti in termini di overhead, poiché condividono la memoria tra i diversi campi e non hanno discriminanti o controllo automatico del ciclo di vita dei dati.

Non insisto oltre su questo argomento perchè usufruiremo molto raramente del tipo union che, come detto, è meglio affrontare in momenti in cui la preparazione sarà più avanzata.