Rust - Le ennuple (tuple)
Argomento di questo paragrafo è un costrutto utile e molto utilizzato
anche se a volte passa un po', come dire, sottotraccia, nel senso che si
sente parlare spesso di array, stringhe, slice ecc... e più raramente
delle ennuple (spesso indicate anche in italiano con il
termine inglese tuple o qualche volta anche n-uple.
Io in questo paragrafo adopererò la definizione nella nostra lingua).
Iniziamo a dare una definizione formale, ne troverete di molto simili
sul web:
Una ennupla è una collezione ordinata di valori di
tipi potenzialmente diversi, racchiusi tra parentesi
tonde.
Il tipo standard di una tupla è della forma:
Un'altra definizione è:
una ennupla è una collezione di valori di tipi
anche diversi raccolti in un unico tipo composto
Abbiamo già visto in azione una ennupla? Certo che si, basti ricordare la
inizializzazione multipla nel capitolo relativo alle variabili e costanti.
Avevamo visto questa definizione formale:let (var-1, var-2, var-3... var-n)
= (valore-1, valore-2, valore-3,... valore-n)
specificando che a destra
dell'operatore di assegnazione c'era una ennupla, evidenziata.
In realtà per definire una nuova ennupla abbiamo due modi, come avrete intuitol, lasciando fare all'inferenza oppure no:
let enumnome : (tipo-1, tipo-2,..., tipo-n) = (valore-1, valore-2, ...,
valore-n);
let enumnome = (valore-1, valore-2, ..., valore-n);
Trattandosi di collezione ordinata c'è una indicizzazione, come di consueto
per questi tipi, ne vedremo altri, tale indicizzazione inizia da 0 e prosegue
sequenzialmente.
Anche le ennuple sono per natura immutabili
ed è necessario inserire il solito mut per
renderle modificabili, niente di nuovo.
Vediamo un picclo esempio di base:
Esempio 12.1
fn main() {
let t01 = (1, 'a', "aaa");
println!("{:?}", t01);
}
questo semplice codice espone una tuple nel suo completo sviluppo
| (1, 'a', "aaa") |
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. Troviamo una ennupla in
fondo e non è affatto un caso, come vedremo studiando le funzioni.
Una tupla con un solo elemento deve comunque contenere la virgola:
(8,) è una tupla con un elemento(8) è il valore 8
e basta
Le ennuple si
distinguono per numero di elementi e per il loro tipo in
conseguenza di ciò ad eempio:
(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. E neanche sono struct. Quindi non utilizzateli in questo senso. Sono
dei contenitori che possono risultare utilissimi in molte situazioni, ma,
ripeto, non sono strutture dati maneggevoli. In particolare quando le
useremo:
Quindi, in quali casi
possiamo favorevolmente utilizzare questo costrutto? Principlamente in
queste situazioni
--- i valori sono pochi e logicamente collegati;
---
l’uso è locale e non serve documentare i campi;
--- Restituire più
valori da una funzione.
--- Passare un gruppo di valori a una funzione con un
singolo parametro.
--- Creare piccole strutture dati senza la necessità di
definire una struct.
--- Inizializzazione
multipla di più variabili Come abbiano visto. In quel caso
operiamo una destrutturazione della ennupla.
Per
natura 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
Esempio 15.2
fn main() {
let mut t01 = (1, 'a', "aaa");
println!("{:?}", t01);
t01.1 = 'b';
println!("{:?}", t01);
}
che ci restituisce:
| (1, 'a', "aaa") (1, 'b', "aaa") |
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
certo notato che per accedere agli elementi di una ennupla abbiamo usato indici
numerici espliciti preceduti dal punto. Quella è l'unica strada permessa,
l’indice deve essere un literal numerico, non un’espressione. Anche una
costante non è ammessa. Inoltre l'indice deve
essere noto a compile time. 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`
--> r170.rs:3:18
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`
| required by this formatting parameter
= help: the trait `Debug` is not
implemented for `({integer}, {integer}, {integer}, {integer}, {integer},
{integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer},
{integer})`
= help: the following other types implement
trait `Debug`:
()
(A, Z, Y, X, W, V, U, T)
(B, A, Z, Y, X, W, V, U, T)
(C, B, A, Z, Y, X, W, V, U, T)
(D, C, B, A, Z, Y, X, W, V, U, T)
(E, D, C, B, A, Z, Y, X, W, V, U, T)
(T,)
(U, T)
and 5 others
error: aborting due to 1 previous error
For more information about this error,
try `rustc --explain E0277`.
Che dice fin troppo per i nostri scopi fornedoci una spiegazione completa.
In assoluto le ennuple limitate 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
Riassumendo, Il limite di 12 elementi riguarda solo le implementazioni
automatiche nella std. In futuro potrebbe cambiare.
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. Detto che clone è sempre disponibile (in realtà se la ennupla è
copy allora i due netodi sono praticamwnte coincidenti) qualora gli elementi interni
supportino Copy è possibile utilizzare quel trait per la copia. In entrambi
i casi avremo copie indipendenti ma in linea di massima personalmente
userei
clone soltanto laddove non sia disponibile copy che a mio avviso è
istruzione più chiara e
idiomatica. A dire il vero sul web ho trovato alcuni utenti che affermano
che clone sia più efficiente. Non effettuato test in merito. Comunque, per esemplificare:
let a = (10, 20);
let b = a; // bitwise copy
let c =
(String::from("ciao"), vec![1,2,3]);
let d = c.clone();
Evidenziamo il fatto che copy effettua usa una copia bit a bit.
Di seguito un esempio per illustrare una volta di più il rigido sistema di regole di borrowing e ownership di questo linguaggio applicato alle ennuple basandoci sulla possibilità di inizializzare più variabili contemporaneamente.
Esempio 15.3
fn main() {
let t01 = ("uno".to_string(), "due".to_string());
println!("{:?}", t01);
let (a,b) = t01;
println!("{}", a);
println!("{:?}", t01);
}
Questo codice non compila. Infatti il compilatore ci dice:
error[E0382]: borrow of partially moved
value: `t01`
-->
r172.rs:6:22
4 | let
(a,b) = t01;
| -
value partially moved here
5 | println!("{}", a);
6 | println!("{:?}", t01);
| ^^^ value borrowed here after partial move
= note: partial move occurs because `t01.1` has
type `String`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid
moving the value
4 |
let (a,ref b) = t01;
| +++
Esatto. L'operazione di assegnazione muove gli elementi della ennupla fuori
dal controllo della stessa, perchè abbiamo a che fare con delle string
che non implementano copy. Quindi quando il controllo passa alle variabili
viene sottratto alla ennupla. Interessante la soluzione proposta dal
compilatore. In pratica se la riga let(a, b) = t01; diventalet(ref
a, ref b) = t01;
tutto funziona. Questo perchè
ref è un modificatore di binding che crea un legame per
riferimento creando quindi un borrow. In questo modo le cose
tornano a posto.
Un piccolo trucco, banale in realtà, ancora nel caso di inizializzazione di più variabili. Se la ennupla che volete abbinare alle variabili è composta da n elementi e le variabili sono n-1 dovete la variabile di scarto _ ad esempio:
let t01 = (1,2,3,4);
let (a,_,c) = t01;
Con le ennuple ci rivedremo quando parleremo delle funzioni. E lì sarà casa loro.