|
Gli array, come intuibile dal loro stesso nome e come avviene in molti altri linguaggi, sono sequenze. In particolare, si tratta di sequenze di elementi dello stesso tipo indicizzati tramite interi sequenziali che iniziano da 0. Semanticamente la definzione è la seguente: [T; N] laddove T rappresenta il tipo degli elementi ed N il loro numero, ovviamente un intero positivo. Gli array in Rust sono immutabili e quindi non è possibile aggiungere o togliere elementi dagli stessi. Per N è ammesso il valore 0 che quindi crea un array vuoto mentre è da notare che [T; N1] è diverso da [T; N2] in quanto sia il tipo sia il numero di elementi sono caratteristiche peculiari di ogni array. Sintatticamente abbiamo due modi per definire direttamente un array: 1) [valore1, valore2, ...., valoreN] 2) [espressione; numero-elementi] Nel caso 2, come da documentazione ufficiale, espressione deve implementare il trait copy oppure essere una costante. Quest'ultimo fatto lo potete verificare in maniera facile, ad esempio: let ar01 = ["aa".to_string(); 5]; non compila e l'errore è, come sempre, illuminante: error[E0277]: the trait bound `String: Copy` is not satisfied --> r172.rs:2:17 | 2 | let ar01 = ["aa".to_string(); 5]; | ^^^^^^^^^^^^^^^^ the trait `Copy` is not implemented for `String` | = note: the `Copy` trait is required because this value will be copied for each element of the array = help: consider using `core::array::from_fn` to initialize the array = help: see https://doc.rust-lang.org/stable/std/array/fn.from_fn.html for more information La riga in neretto vi spiega anche il perchè dell'errore. Sappiamo, dal capitolo relativo, che le stringhe non implementano il trait Copy. In memoria gli array sono conservati in blocchi di memoria consecutivi, come detto gli elementi sono indicizzati da 0 in avanti attraverso numeri interi. Ad esempio l'array ['a', 'b', 'c', 'd'] può essere rappresentato come segue:
Riassumendo:
let ar1 : [u8, 3] = [1,2,3]; rispettando come sempre i limiti imposti dal tipo scelto. E' da notare tuttavia, sempre relativamente al secondo metodo una particolarità che possiamo illustrare tramite il seguente codice: let x = 8; let ar1 = [0; x]; Questo codice non compila è il motivo è semplice: la dimensione di un array deve essere nota a compile-time mentre il valore viene assegnato ad x a run-time quindi, diciamo, troppo tardi. L'errore è chiaro ed "educativo" in quanto ci indica anche una soluzione: error[E0435]: attempt to use a non-constant value in a constant --> r1001.rs:4:19 | 3 | let x = 8; | ----- help: consider using `const` instead of `let`: `const x` 4 | let ar1 = [0; x]; | ^ non-constant value Quindi la soluzione in questi casi è usare una costante, come indicato nella riga evidenziata. Il compilatore come sempre cerca di darci una mano. Tutto semplice? Si ma c'è ancora un problema: const X: i32 = 8; let ar1 = [0; X]; anche questo codice dà origine ad errore e precisamente: error[E0308]: mismatched types --> r1002.rs:4:19 | 4 | let ar1 = [0; X]; | ^ expected `usize`, found `i32` ovvero la costante deve essere di tipo usize non i32. Questo ci porta a concludere che il tipo al quale appartengono gli indici di un array è usize quindi legato all'architettura della macchina (ricordiamo che per scoprire il limite esiste sempre usize::MAX). La soluzione pertanto consiste nel sostituire i32 con usize nella prima riga. Dobbiamo tuttavia fornire una soluzione al caso visto all'inizio, ovvero come inizializzare un array in modo veloce con elementi che non supportano il trait Copy, come le stringhe. Ebbe tornando a quel caso possiamo scrivere una soluzione del genere: let ar01: [String; 5] = std::array::from_fn(|_| "aa".to_string()); è presente in questo codice il concetto di chiusura che ancora non abbiamo visto. Inoltre La funzione std::array::from_fn in Rust è un'utilità che permette di creare un array di dimensioni fisse, inizializzando ciascun elemento dell'array usando appunto una chiusura. Sempre con riguardo alla inizializzazione, array fino a 32 elementi supportano il trait Default ed è possibile assegnare un valore di default se il tipo scelto per i singoli elementi lo supporta: let x1 : [i32; 32] = Default::default(); println!("{}", x1[3]); ne ricaveremo un bello '0' a video. Se proviamo a definire l'array precedente con dimensione 33 o superiore il programma non compilerà. Ancora, è possibile, fino a 12 elementi passare da un tuple ad un array tramite From: fn main() { let tup = (1, 2, 3, 4); let arr: [i32; 4] = From::from(tup); println!("{:?}", arr); } E' il momento di vedere come manipolare i nostri array: Accedere ad un elemento di un array è molto semplice tramite l'uso dell'operatore [] let ar1 = [1,2,2000]; println!("{}", ar1[1]); ovviamente cercare di accere ad indici fuori range provoca un errore in compilazione, come il seguente dove ho imposto indice 5: error: this operation will panic at runtime --> r983.rs:4:18 | 4 | println!("{}", ar1[5]); | ^^^^^^ index out of bounds: the length is 3 but the index is 5 se l'indice è proposto a runtime avrete un crash (panic) del programma. Se volete evitare questo dovrete usare get(indice) che restituisce un Option (vedi paragrafo apposito):
Il metodo len() è quello usato per ottenere la lunghezza, ovvero il numero di elementi che compongono l'array. let ar1 = [1,2,2000]; println!("{}", ar1.len()); ricaveremo il numero 3. Scorrere gli elementi di un array è molto semplice. Questo task può essere ottenuto sia utilizzando in maniera del tutto intuitiva il ciclo for: let x01 = [1,2,3,4,5]; for i in 0..x01.len() { print!("{},", x01[i]) } sia ricorrendo allo specifico metodo iter(): let x01 = [1,2,3,4,5]; for i in x01.iter() { print!("{},", i) } Se volete potete anche procedere tramite riferimento let x1 : [i32; 32] = [3; 32]; for x in &x1 { println!("{}", x); } per la copia di un array, a parte gestire la cosa in maniera poco efficiente tramite la copia elemento per elemento tramite un ciclo, abbiamo una via migliore che sfrutta il supporto al trait clone quindi: let ar1 = [1,2,3,4]; let ar2 = ar1.clone(); println!("{:?}", ar1); println!("{:?}", ar2); Altrimenti dal momento che gli array supportano il trait copy, il può usare il classico =: let ar2 = ar1; Altri metodi comodi per lavorare utilmente sugli array sono sort() che, laddove sia permesso dal tipo interno ordina in senso crescente, e reverse() che rovescia gli elementi in senso inverso.. Si tratta di metodi utili nella pratica quotidiana.
che fornisce come output:
Come abbiamo detto, il numero di elementi di array [T;N] è immutabile, ribadiamo quindi che non possiamo cancellare o aggiungere elementi, ovvero, in pratica, la lunghezza di un array è una costante a compile time, ma gli elementi all'interno, ferma restando la necessaria compatibilità di tipo, possono essere modificati, sostituiti con altri dello stesso tipo. Il metodo più rapido e semplice fa ancora uso dell'operatore [] oltre che ovviamente dell'immancabile mut per conferire mutabilità all'array: let mut ar1 = [10, 3, 7, 2, 18]; ar1[1] = 88; e l'elemento all'indice 1 da 3 diventa 88. Questo è il metodo più semplice ma genera un panic se utilizzate un indice errato. Se volete una strada più sicura potete far uso di get_mut(indice), che genera un Option in grado di gestire eventuali indici errati. Vediamo l'esempio:
Provate a vedere cosa succede se alla riga 3 invece dell'indice 2 inserite un indice fuori range, quindi maggiore di 4 in questo caso. Un altro caso interessante è quello delle definizione di array multidimensionali. Sostanzialmente si tratta di array composti da altri array, come nel seguente esempio:
questo esempio propone due array costituiti da 3 elementi. L'output è il seguente:
Ovviamente potete anche complicare la cosa e aggiungere ulteriori dimensioni al vostro array: let v2 = [[[1,2,3];2];3]; println!("{}", v2[1][1][0]); println!("{}", v2.len()); Questo è un array di 3 elementi, ciascuno dei quali è un array di due elementi ciascuno dei quali è composto da 3 elementi. Un po' difficile da immaginare "fisicamente" ma non particolarmente complesso da manipolare. Prima di concludere vediamo i trait implementati dagli array se i tipi che ne costituiscono gli elementi lo permettono: Copy Clone Debug IntoIterator (implementato per [T; N], &[T; N] e &mut [T; N]) PartialEq, PartialOrd, Eq, Ord Hash AsRef, AsMut Borrow, BorrowMut Su questo elenco, come sugli altri analoghi, sarà utile tornare quando avremo trattato i vari Trait. Ci sarebbe ancora materiale per parlare degli array, ad esempio il loro uso come parametri per le funzioni ma lo faremo quando affronteremo proprio le funzioni. Vedremo anche un trucco, nel capitolo "Varie", per creare array con tipi differenti, anche se ovviamente non è esattamente una cosa standard. Di per se stessi avrete capito che si tratta di strutture molto comode e pratiche, che al netto di qualche rigidità, apprezzerete molto nell'uso quotidiano. |