|
Continuando il nostro cammino tra i tipi è arrivato il momento di affrontare le tuple, tipo che troveremo utilissimo in futuro nonostante qualche sua limitazione. Anche questi elementi fanno parte delle sequenze. Una possibile definizione è la seguente: +++ una tuple (o ennupla, come io preferisco chiamarle) è una collezione finita di elementi eterogenei (ovvero anche di tipi diversi) indicizzati. Una definizione non nuova, ovviamente. Oppure più brevemente: +++ è una collezione di valori di tipi anche diversi raccolti in un unico tipo composto. Formalmente abbiamo due possibili definizioni: let enumnome : (tipo-1, tipo-2,..., tipo-n) = (valore-1, valore-2, ..., valore-n); let enumnome = (valore-1, valore-2, ..., valore-n); ovvero una sequenza di valori all'interno di una coppia di parentesi tonde. Nel secondo caso entra in gioco l'inferenza evidentemente. Come detto gli elementi sono indicizzati, al solito tramite numeri interi in sequenza che iniziano da 0. Sottolineiamo che anche le tuple nascono immutabili. per modificarne gli elementi bisogna usare il consueto mut. Vediamo ora un esempio di base:
che presenta il seguente output:
Da notare che una ennupla vuota viene chiamata tipo unit, appunto indicato come (). Quest'ultimo vi ricorda qualcosa? Ma certo, guardate come definiamo ogni volta l'entry point (main) dei nostri programmi, riga 1 dell'esempio 12.1, tanto per dire l'ultimo caso. Le ennuple si distinguono per numero di elementi e per il loro tipo tenendo presente quindi che, per esempio: (i32, char) è diversa da (char, i32) la lunghezza n, ovvero il numero di elementi che compone una tuple è detta "arità". Va stabilito in maniera forte e chiara da subito che le ennuple non sono array (struttura molto più flessibile che vedremo in altro capitolo) e nemmeno ne sono loro validi sostituti. Quindi non utilizzateli in questo senso. Sono dei contenitori che possono risultare utilissimi in molte situazioni, ma, ripeto, non sono strutture dati maneggevoli. Quindi, in quali casi possiamo favorevolmente utilizzare questo costrutto?
Di contro le ennuple sono strutture rigide e la loro manipolazione non è particolamente flessibile. Ad esempio la loro dimensione è fissa e quindi non è possibile nè aggiungere nè togliere elementi. Ovviamente se volete sostituire un elemento dovete immetterne un altro dello stesso tipo. Vediamo un esempio, premettendo che l'accesso al singolo dato di una tuple si ha con il formato: nometuple.indice
dà come output:
se però provassimo a sostituire l'istruzione alla riga 4 come segue: t01.1 = 3; il compilatore risponde come si deve: error[E0308]: mismatched types --> r148.rs:4:14 | 4 | t01.1 = 3; | ----- ^ expected `char`, found `u8` | | | expected due to the type of this binding Non funziona pertanto l'operatore [], come avviene invece per le stringhe e ciò forse avvicinerebbe le ennuple alle struct, più che alle sequenze. Insomma le ennuple sono un po' varie come natura. Avrete notato che per accedere agli elementi di una ennupla abbiamo usato indici numerici espliciti. Quella è l'unica strada permessa, non potrete usare nè una variabile nè una costante per tale operazione. Anche questa è una limitazione abbastanza pesante. Ad esempio non potrete usare una normale iterazione sugli elementi di una ennupla. E' inoltre parecchio complicato anche estrarre il numero degli elementi al suo interno. Se però vi state facendo queste domande, ovvero come iterare sugli elementi o recuperarne il numero, è molto probabile che stiate usando la struttura sbagliata per conservare i vostri dati. Un metodo pratico, se proprio vi dovesse servire contare gli elementi di una ennupla e questa ha tutti gli elementi dello stesso tipo, è creare un vettore a partire da essa e usare i metodi tipici per i vettori stessi: fn main() { let my_tuple = (1, 2, 3); let my_vector: Vec<_> = vec![my_tuple.0, my_tuple.1, my_tuple.2]; let tuple_length = my_vector.len(); println!("La lunghezza della tupla è: {}", tuple_length); } se volete potete rivedere questo codice dopo che avremo affrontato i vettori. Ma ovviamente la sua utilità è comunque molto limitata. Sul Web troverete molte soluzioni nessuna delle quali definitiva, almeno tra quelle che ho trovato io. Francamente non è un problema su cui perdere troppo tempo. Un'altra limitazione delle ennuple è che esse supportano certi trait fino ad una arità pari a 12. Ad esempio il banale println! su una ennupla di 13 elementi let t01 = (1,2,3,4,5,6,7,8,9,0,1,2,3); println!("{:?}", t01) restituisce questo verboso responso in compilazione: error[E0277]: `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` doesn't implement `Debug` --> r166.rs:3:22 | 3 | println!("{:?}", t01) | ^^^ `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` cannot be formatted using `{:?}` because it doesn't implement `Debug` | = help: the trait `Debug` is not implemented for `({integer}, ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., ...)` = help: the following other types implement trait `Debug`: () (C, B, A, Z, Y, X, W, V, U, T) (T,) (U, T) (E, D, C, B, A, Z, Y, X, W, V, U, T) (V, U, T) (W, V, U, T) (B, A, Z, Y, X, W, V, U, T) and 5 others = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0277`. che dice anche troppo.... In assoluto le ennuple fino a 12 elementi supportano i seguenti trait: PartialEq Eq PartialOrd Ord Debug Default Hash mentre per tutti valgono questi: Clone Copy Send Sync Unpin UnwindSafe RefUnwindSafe Questa situazione potrebbe cambiare in versioni future. Tra tutti per i nostri scopi è interessante il supporto al trait Copy e al trait Clone cosa che ci fa capire che possiamo effettuare delle copie di una tuple. Di solito l'uso clone() è il sistema più efficiente. let t01 = (1,2,3,4,5); let t02 = t01; let t03 = t01.clone(); Abbiamo detto che grazie alle tuple siamo in grado di inizializzare più variabili in modo veloce tramte la destrutturazione. Bisogna fare però attenzione perchè le regole dell'ownership sono sempre dietro l'angolo. Considerate il seguente esempio:
questo programma compila solo se la riga 6 è commentata. Se eliminate il commento ne risulta un errore: error[E0382]: borrow of partially moved value: `t01` --> r171.rs:6:22 | 4 | let (a,b) = t01; | - value partially moved here 5 | println!("{}", a); 6 | println!("{:?}", t01); | ^^^ value borrowed here after partial move il problema è sempre lo stesso, le String non implementa il trait copy come detto e quindi i valori sono mossi da t01 ad a e b quindi t01 non è più utilizzabile. Per ora è tutto sulle ennuple. Le rivedremo pesantemente in azione quando parleremo delle funzioni, per le quali rappresentano i partner ideali, come abbiamo già ampiamente accennato. |