Rust - gli array
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. Vengono immagazzinati in memoria contigua.
Semanticamente la definzione è la seguente:
[T; N]
laddove T rappresenta il tipo degli elementi ed N
il loro numero, ovviamente un intero positivo che è una costante a compile
time. Quindi anche la dimensione è
nota a compile time e questo come vedremo è un elemento non secondario. Gli array in Rust sono
immutabili dal punto di vista dimensionale 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) [valore-1, valore-2, ...., valore-N]
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 evidenziata vi spiega anche il perchè dell'errore.
Sappiamo, dal capitolo relativo, che le stringhe non implementano il trait
Copy. Quindi non può avvenire la proliferazione della stringa in questa
forma.
In memoria gli array come detto all'inizio, sono conservati in blocchi di memoria
consecutivi e gli elementi sono indicizzati da 0 in avanti
attraverso numeri interi. Ad esempio l'array ['a', 'b', 'c', 'd'] può
essere rappresentato come segue:
| array | a | b | c | d |
| indice | 0 | 1 | 2 | 3 |
Riassumendo:
--- gli array definiti in questo
paragrafo sono elementi statici, ovvero una volta definiti
non possono essere aumentati ne diminuiti per quanto riguarda il numero di
elementi che lo compongono
--- i dati sono contenuti in blocchi
consecutivi di memoria
--- ogni blocco rappresenta un elemento dell'array
ed è caratterizzato da un indice
aggiungiamo che:
--- gli array
sono contenuti nello stack il che rende molto veloce la loro manipolazione.
Ovviamente però se un array contiene elementi che devono "abitare" nello
heap, ad esempio delle stringhe, verranno coinvolti sia lo stack che lo
heap. Nell'esempio, nello stack avremo puntatore + lunghezza + capacità,
nello heap avremo gli elementi della stringa.
--- si tratta di un tipo
"sicuro" sia per quanto concerne gli accessi oltre i limiti sia per le
elaborazioni, stante l'omogenietà dei tipi e i controlli rigorosi che in
questo modo il compilatore può mettere in atto.
Per inizializzare un
array, come detto, abbiamo sostanzialmente due modi:
--- specificare i
singoli elementi che lo compongono:let ar1 = [1,2,3,4];
---
specificare un singolo elemento ripetuto n volte:let ar1 = ['a'; 5];
Il secondo sistema crea un array contenente 5 volte il carattere 'a'. Possiamo anche forzare la natura degli elementi senza lasciar fare all'inferenza:
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];
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 il concetto di chiusura che ancora non
abbiamo visto. La funzione std::array::from_fn in Rust è un'utilità
che permette di creare un array di dimensioni fisse, inizializzando ciascun
elemento dell'array tramite appunto una chiusura. Ne parleremo delle
chiusure che sono un altro argomento piuttosto importante, non solo in Rust.
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:
Esempio 13.1
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 accedere
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. Questo come sempre a tutela della solidità del programma, non ci sono overflow o comportamenti anomali, i famosi UB, e quindi pericolosi.
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, dalla funzione di stampa, 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);
}
Non ci sono sostanziali differenza pratiche tra questi codici.
Ricavare la copia di un array dipende anche dal tipo
degli elementi interni. E, come avrete intuito, non è cosa che non nasconda
qualche insidia.
Il caso più semplice è come sempre quello che prevede
all'interno dell'array elementi che implementano il trait Copy. In tale caso
l'operatore = va benissimo:
Esempio 13.2
fn main() {
let ar1 = [1,2,3,4];
let ar2 = ar1;
println!("{:?}", ar1);
println!("{:?}", ar2);
}
da cui otteniamo
| [1, 2, 3, 4] [1, 2, 3, 4] |
stesso risultato se invece dell'operatore di assegnazione avessimo usato clone() scrivendo:
let ar2 = ar1.clone();
d'altronde in Rust come sappiamo Clone è un supertrait di Copy.
Laddove invece all'interno dell'array vi siano elementi che non implementano
Copy allora le cosa cambiano. Se infatti ar1 nelle esempio 13.2 fosse:
let ar1 = ["ciao".to_owned(), "mondo".to_owned()];
dopo averne effettuato la copia con
let ar2 = ar1;
l'istruzione di stampa di ar1 andrebbe in errore di compilazione:
borrow of moved value: `ar1`
--> r178.rs:4:18
2 | let ar1 = ["ciao".to_owned(),
"mondo".to_owned()];
| --- move occurs because `ar1` has type `[String;
2]`, which does not implement the `Copy` trait
3 | let ar2 = ar1;
| --- value moved here
4 | println!("{:?}", ar1);
| ^^^ value borrowed here after move
help: consider cloning the value if the
performance cost is acceptable
3 | let ar2 = ar1.clone();
| ++++++++
con tanto di soluzione proposta nella riga evidenziata.
let ar1 = ["ciao".to_owned(), "mondo".to_owned()];
let ar2 =
ar1.clone();
println!("{:?}", ar1);
println!("{:?}", ar2);
così funziona.
Infatti clone() è il caso più generale come detto e le stringhe implementano
il trait Clone.
Il caso peggiore però avviene quando l'array è costituito
da elementi che non implementano nemmeno clone(), ad esempio quando
definiamo un array di tipo custom. Lì le cose si fanno più complicate perchè
è necessario ricorrere ad implementazioni manuali. Un caso è il seguente
(nb:
vedrete del codice che non probabilmente non vi è chiaro: certi concetti
verrano spiegati più avanti).
Esempio 13.3
fn main() {
#[derive(Debug, Clone)]
struct Punto {
x: f64,
y: f64,
}
let p01 = Punto { x: 1.0, y: 2.0 };
let p02 = Punto { x: 3.0, y: 4.0 };
let ar1 = [p01, p02];
let ar2 = ar1.clone();
println!("{:?}", ar1);
println!("{:?}", ar2);
}
Ignorate i warning in compilazione, concentratevi solo sul fatto che
abbiamo messo il trait Clone a disposizione della struct e quindi anche
dell'array.
Casi ancora più complicati sono quelli in cui Clone deve
essere implementato manualmente oppure, peggio ancora, quei rari casi in cui
Clone non è implementabile per gli elementi dell'array ma qui si va un
territorio troppo complesso per il momento.
Altri metodi comodi per lavorare utilmente sugli array sono sort() che, laddove sia permesso dal tipo interno, ordina in senso crescente, reverse() che rovescia gli elementi in senso inverso e contains(&elemento) (si noti che richiede un puntatore ad un valore... questo perchè la sua firma è unica per tutti i i tipi e in questo modo evitiamo dei move inutili) che ci dice se l'array contiene o no un certo elemento. Si tratta di metodi utili nella pratica quotidiana e li vediamo tutti insieme nel seguente codice:
Esempio 13.4
fn main() {
let mut ar1 = [3, 5, 1, 17, 9, 0];
ar1.sort();
println!("{:?}", ar1);
ar1.reverse();
println!("{:?}", ar1);
println!("esiste il numero 5? {}", ar1.contains(&5));
}
Da cui abbiamo:
| [0, 1, 3, 5, 9, 17] [17, 9, 5, 3, 1, 0] esiste il numero 5? true |
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.
Tuttavia 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:
Esempio 13.5
fn main() {
let mut arr = [1, 2, 3, 4, 5];
match arr.get_mut(2) {
Some(val) => *val = 42, // Modifica l'elemento all'indice 2
None => println!("Indice non valido!"),
}
println!("{:?}", arr); // [1, 2, 42, 4, 5]
}
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. (Lo so non sappiamo ancora cos'è il tipo Option... ci arriveremo)
Altro argomento ancora sono gli array multidimensionali. Sostanzialmente si tratta di array di array come nel seguente esempio:
Esempio 13.6
fn main(){
let v2 = [[1,2,3];2];
println!("{}", v2[1][1]);
println!("{}", v2.len());
}
questo esempio propone due array costituiti da 3 elementi. L'output è il seguente:
| 2 2 |
in cui il secondo "2" indica le dimensioni dell'array che è costituito proprio da 2 array.
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.
Array e slice
Come si confrontano array e slice? Vediamo il seguente
codice per passare da array a slice:
let a = [1, 2, 3]; // array [i32; 3], vive sullo stack
let s: &[i32] = &a; // slice: riferimento a una porzione di 'a'
Qui abbiamo definito un array e successivamente una slice che punta ad una porzione dell'array. Come sempre la slice non possiede l'array o la sua porzione ma diciamo che la osserva, avendola in prestito grazie al puntatore. Ci si può sbizzarrire nel costruire slice aventi diversa ampiezza:
let a = [1, 2, 3, 4, 5];
let s1: &[i32] = &a; // [1,2,3,4,5] —
tutto
let s2: &[i32] = &a[1..4]; // [2,3,4] — porzione
let s3: &[i32]
= &a[..2]; // [1,2] — inizio
let s4: &[i32] = &a[3..]; // [4,5] — fine
E' molto utile quando dobbiamo passare porzioni di array ad una funzione. Diversamente si dovrebbe costruire una funzione diversa per ogni dimensione del parametro da passare.
Il passaggio inverso è più problematico e lo capite da soli: un array porta co sè a compile time l'informazione della dimensione, una slice sappiamo che non la porta. Quindi bisogna fare qualche controllo in più. In pratica bisogna verificare che la lunghezza runtime della slice (o della porzione che vogliamo considerare) coincida con quella dell'array che abbiamo definito. Tipicamente si usa la funzione try_into():
let s: &[i32] = &[1, 2, 3, 4, 5];
let a: [i32; 3] =
s.try_into().unwrap(); // panico: slice ha 5 elementi, non 3
let a:
[i32; 5] = s.try_into().unwrap(); // dimensioni coincidono
Questo è un modo veloce e funzionale, teniamo presente che
try_into() restituisce un result
(si, lo devo dire anche qui, lo vedremo più avanti) che permette di gestire
meglio l'errore, non bisogna usare unwrap, ma come detto lo vedremo.
altra possibilità è usare try_from(slice)
let arr = <[u8; 3]>::try_from(slice).unwrap();
Array e Vec!
E' argomento che vediamo nel prossimo
paragrafo.
Array ed ennuple
Di norma non è possibile effettuare una conversione
diretta perchè le ennuple sono eterogenee a livello di tipologia dei propri
elementi. E' possibile quindi quando sia uniofrmità ma non esiste modo
idiomatico, bisogna fare a mano:
slice -> arraylet t: (i32, i32, i32) = (1, 2, 3);
let a: [i32; 3] = [t.0, t.1,
t.2];
array -> slicelet a: [i32; 3] = [1, 2, 3];
let t: (i32, i32, i32) = (a[0], a[1],
a[2]);
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.
Destrutturazione di un array
Possiamo usare un array
per inizializzare più variabili, con varie possibilità e stando attenti a
qualche regola di base.
let ar1 = [1,2,3];
let [a, b, c] = ar1;
Questo è il caso più semplice. Abbiamo 3 variabili inizializzate con i valori dell'array che resta utilizzabile a sua volta. Se però il trait copy non è implementato dagli elementi interni, come nel caso:
let arr = [String::from("a"), String::from("b")];
let [s1, s2] = arr;
// OK: i valori vengono *mossi*
allora arr non è più utilizzabile perchè le stringhe non implementano Copy e quindi bisogna fare attenzione.
Se vi servissero solo due valori, ad esempio il primo e il terzo potrete usare la variabile di scarto:
let [a, _, c] = ar1;
Vi potreste chiedere dove "vive" un array. Ebbene la sua "casa" è lo stack, come abbiamo visto possono anche esserci elementi al suo interno che vanno sullo heap ma nello stack avremo sempre i descrittori ( ad esempio, puntatore, dimensioni e capacità) e saranno quelli la parte vera e propria dell'array. Questo a meno di non ricorrere ad operazioni di boxing che vedremo nell'apposito paragrafo e che in effetti permetteranno di posizionare un array sullo heap.
Tutto qui? No. Gli array ad esempio vanno molto d'accordo con le funzioni e questo sarà un argomento che tratteremo in maniera particolareggiata nell'apposito paragrafo.