Rust - I vettori
In Rust, Vec<T> è probabilmente il
contenitore dinamico fondamentale:
se un array [T; N] rappresenta una
sequenza di elementi dello stesso tipo di lunghezza fissa,
Vec<T> rappresenta una sequenza di elementi,
sempre dello stesso tipo, la cui lunghezza può cambiare
durante l'esecuzione. Evidenziamo le differenze tra i due costrutti:
Dimensione: gli array hanno una dimensione fissa che
deve essere nota al momento della compilazione e non può essere modificata
in seguito. I vettori, invece, sono dinamici e possono crescere o diminuire
durante l’esecuzione del programma. Il formalismo che li definisce è Vec
Tipo di Allocamento: Gli array sono di norma allocati sullo stack (in realtà se sono dentro una struct o boxati possono finire sullo heap) e la loro dimensione deve essere nota a tempo di compilazione (fa parte del tipo). I vettori sono allocati sullo heap, permettendo loro di avere una dimensione variabile e di espandersi all’occorrenza.
Sintassi: Esistono differenze a livello dichiarativo. Abbiamo visto nel capitolo 13 come dichiarare gli array, qui ilustreremo i vari sistemi possibili con i vettori e ci renderemo conto delle differenze.
Performance: Gli array possono offrire prestazioni leggermente migliori per via della loro allocazione sullo stack. I vettori possono avere un overhead di prestazioni dovuto alla loro natura dinamica e alla gestione della memoria sull’heap.
Uso: Gli array sono ideali quando si conosce la dimensione del dataset e questa non è destinata a cambiare. I vettori sono più adatti quando si ha bisogno di una collezione che può cambiare dimensione in pratica quando serve uno strumento duttile grazie alla sua variabilità.
Come avrete forse già intuito, anche i vettori sono indicizzati in maniera sequenziale tramite interi partendo da 0. Tali interi sono in realtà di tipo usize, come per gli array.
Iniziamo quindi vedendo come si inizializzare un array.
1) usando newlet v1: Vec<i32>
questa istruzione
crea un array vuoto (e tale resta essendo immutabile). Non essendoci
elementi al suo interno il tipo deve essere specificato in quanto
evidentemente l'inferenza non saprebbe cosa fare.
2) usando la macro vec!let v1 =
vec![1,2,3];
qui abbiamo creato
un vettore di 3 elementi sui quali il compilatore inferisce il tipo i32.
Perchè si usa una macro? Per il semplice fatto che una funzione costruttore
avrebbe il problema di gestire un numero variabile di elementi (una volta ne
inseriamo 1, una volta 5, una volta 100 o quanti ne vogliamo insomma ogni
volta) e Rust
non dispone di funzioni variadiche in senso stretto, come vedremo quando
avremo a che fare con le funzioni, per simulare le quali bisogna ricorrere a
qualche contorsionismo. La macro è lo strumento ideale per
risolvere il problema, vale anche come abbiamo visto per la funzione di
stampa.
Tramite questa macro è anche possibile creare un vettore di N
elementi tutti uguali, genericamente:
let v1 = vec![X; N]
l'array v1 è composto da N elementi X.
Su questa base potrete anche scrivere una sequenza di istruzioni così:
let x = 4;
let v1 = vec![1;x];
cosa che non è possibile con gli array la cui dimensione deve essere a
nota compile time.
E' invece interessante notare se specificate nella prima
riga: let x: i32 = 4;
il compilatore si lamenta. Questo perchè,
come abbiamo già detto, l'indice dei vettori, esattamente come per gli
array, è di tipo usize.
In breve, quali sono le differenze principali rispetto ad un array? proviamo a schematizzare la cosa:
| Array | Vec |
|
|---|---|---|
| Tipo | [T; N] |
Vec<T> |
| Lunghezza | fissa | variabile |
| Dimensione | nota nel tipo | non fa parte del tipo |
| Crescita | no | sì |
| Memoria | generalmente inline | elementi in memoria allocata dinamicamente |
Accesso con [] |
sì | sì |
| Indicizzazione | usize |
usize |
Detto questo iniziamo a lavorare con i vettori.
Una prima semplice
manovra è quella di aggiungere elementi ad un vettore vuoto ed ovviamente
mutabile:
Esempio 22.1
fn main() {
let mut v1: Vec = Vec::new();
v1.push(0);
v1.push(1);
v1.push(2);
print!("{:?}", v1);
v1.pop();
print!("{:?}", v1);
}
usiamo la funzione push(elemento) che aggiunge un elemento (ovviamente di tipo compatibile con quello dicharato per il vettore) in coda al vettore. Esiste anche pop() come nell'esempio 22.1 che fa l'opposto, ovvero elimina un elemento in coda al vettore.
Altri tre metodi molto usati sono
- - insert(indice, elemento) che inserisce un elemento un certa posizione
- - remove(indice) che rimuove l'elemento che si trova alla posizione indicata dal parametro indice.
- - len() che riporta il numero di elementi del vettore.
Vediamo un esempio riepilogativo:
Esempio 22.2
fn main(){
let mut v1: Vec = Vec::new();
v1.push(4);
v1.push(5);
v1.push(0);
v1.push(7);
for x in 0..v1.len() {
println!("elemento {} = {}", x, v1[x]);
}
println!("---------");
v1.insert(2, 9);
for x in 0..v1.len() {
println!("elemento {} = {}", x, v1[x]);
}
println!("---------");
v1.remove(2);
v1.remove(3);
v1.pop();
for x in 0..v1.len() {
println!("elemento {} = {}", x, v1[x]);
}
}
Il consueto operatore [x] è quello tramite il quale si può individuare un elemento avente indice x. Un semplice esempio per chiarirne l'uso ed intanto vediamo all'opera anche len():
Esempio 22.3
fn main(){
let v1 = vec!['q', 'b', 'c', 'd', 'e', 'f'];
println!("Elemento indice 2: {}", v1[2]);
println!("Lunghezza: {}", v1.len());
}
Ovviamente il linguaggio vi protegge nel caso in cui indicaste un valore
dell'indice fuori dal range ammesso per il vettore su cui lo applicate e lo fa
nel solito modo molto esplicito, se nell'esempio 22.3 avessimo richiesto
l'elemento all'indice 10 chiamandov1[10]
avremmo ottenuto a runtime:
thread 'main' (67308) panicked at
r305.rs:3:39:
index out of bounds: the len is 6 but the index is 10
note: run with `RUST_BACKTRACE=1` environment
variable to display a backtrace
r305.rs è il nome del file che contiene il codice sorgente. Va da sè che dovremo imparare a gestore queste situazioni. Stessa regola vale per insert e per remove visti in precedenza ovviamente.
Rust comunque non ci lascia soli anche in questo caso e prevede comunque due metodi più avanzati per lavorare sul singolo elemento in maniera più sicura rispetto a [ ]. Si tratta di get(indice). Vediamo un po' di codice:
fn main() {
let v = vec![1, 2, 3, 4, 5];
let r =
v[8];
print!("{}", r);
}
questo programma compila ma ne risulta un errore a runtime:
thread
'main' panicked at r348.rs:3:20:
index out of bounds: the len is 5 but
the index is 8
note: run with `RUST_BACKTRACE=1` environment variable to
display a backtrace
In questo caso se viene selezionato un indice
fuori dal range abbiano un crash (panico) da parte del programma. Vediamo
una versione che fa uso di get:
fn main() {
let v =
vec![1, 2, 3, 4, 5];
let r: Option<&i32> = v.get(8);
match r {
Some(r) => println!("{}", r),
None
=> println!("Fuori range"),
}
}
In questo caso il
programma non va in crash ma ci dice semplicemente che siamo fuori range.
Come per gli array, abbiamo a disposizione get_mut(indice)
per sostituire un elemento in modo sicuro tramite la generazione di un tipo
Option che ci aiuta a gestire situazioni anomale, analogamente a quanto
abbiamo appena visto
Una cosa interessante è la seguente, presentata nelle seguenti due righe di codice ed è una criticità che avevamo già incontrato quando abbiamo parlato degli array:
let v1 = vec![String::from("a"), String::from("b")];
let s = v1[0]; //
ERRORE di compilazione!
questo perchè s prenderebbe possesso dell'elemento all'indice 0 minando l'integrità del vettore, in pratica ci sarebbe un buco. Soluzioni? Eccone 3:
let s = v1[0].clone(); // clona la stringa (costoso ma sicuro)
let s = &v1[0]; // prendi un riferimento invece di possederla
let s =
v1.remove(0); // rimuovi davvero l'elemento dal vettore
Se ricordate gli array supportano solo le prime due soluzioni, remove non è ovviamente ammesso perchè gli array non possono veder modificate le proprie dimensioni.
Con i tipi che supportano Copy questo ovviamente non è un problema.
Attraversare un vettore può essere fatto in vari modi e, come
prevedibile, ciascuno di essi ha la sua peculiarità che permette tipi di
manipolazioni diverse.
1)
Il primo modo è quello che viene naturale pensare
e fa uso del comodo for:
let v1 = vec![1,2,3,4];
for x in v1.iter() {
println!("{}", x);
}
Chiamiamo esplicitamente il metodo .iter(), che produce un iteratore di riferimenti (&i32). v1 resta utilizzabile dopo il ciclo perché non ne prendiamo possesso, solo in prestito dal momento che operiamo per riferimento.
2)
Sempre usando un riferimento ecco un secondo metodo:
let v1 = vec![1,2,3,4];
for x in &v1 {
println!("{}", x);
}
Qui non chiamiamo iter() esplicitamente ma il risultato è identico al precedente in quanto la chiamata a iter() avviene internamente e quindi siamo in una condizione assoltamente equivalente al caso precedente.
3)
Diverso è il terzo caso identico al 2) ma senza
puntatore:
let v1 = vec![1,2,3,4];
for x in v1 {
println!("{}", x);
}
siamo in una situazione diversa perchè il ciclo chiama implicitamente v1.into_iter(), che consuma il vettore prendendone possesso (il solito ownership). Il vettore viene "smontato" elemento per elemento, e x è di tipo i32 (valore diretto, non riferimento). Dopo questo ciclo, v1 non è più utilizzabile: se proviamo a usarlo di nuovo dopo, il compilatore ci dà errore:
error[E0382]: borrow of moved value: `v1`
4)
volendo esiste un modo non troppo idiomatico, io
lo definisco vintage, per scorrere gli elementi di un vettore senza
prenderne possesso. Questo metodo si basa sulla lunghezza del vettore in
modo molto semplice:
let v1 = vec![1,2,3,4];
for i in 0..v1.len() {
println!("{}", v1[i]);
}
Questo si basa sullo scorrimento di un range non direttamente sul vettore. In pratica ogni chiamata
v1[i]
equivale a*v1.index(i)
che in pratica è un prestito temporaneo. Avere il controllo sul range usato è fondamentale per evitare di cadere nei panic del programma.
Finora abbiamo visto casi in cui il vettore originale era lasciato inalterato. Ma se volessimo effettuare direttamente delle modifiche su di esso? Quello che mi sento di consigliare, idiomatico e semplice, iter_mut():
Esempio 22.4
fn main(){
let mut v1 = vec![1, 2, 3];
for x in v1.iter_mut() {
*x += 10; // dereferenziazione
}
println!(" {:?}", v1);
}
Ovviamente dobbiamo dereferenziare per accedere al valore ma questo lo vedremo meglio nel capitolo dedicato ai puntatori. Dal codice 22.4 ricaviamo:
| [11, 12, 13] |
ovvero il vettore, definito mutabile, ha i suoi elementi aumentati di 10 unità.
Rust è Rust, ha le sue regole e quindi dobbiamo vedere cosa succede se invece di avere un vettore ad esempio di interi, ci troviamo davanti ad un vettore ad esempio di stringhe, ovvero elementi di natura decisamente diversa.
I casi 1) 2) 3) e 4) funzionano alla stessa maniera che abbiamo visto anche con vettore ad esempio definito come segue:
let v1 = vec![String::from("a"), String::from("b"), String::from("c")];
basta provare a sostituire il vettore v1 precedentemente definito e ne avrete la riprova. Per fortuna qui non ci sono frosse novità, l'unica differenza la abbiamo vista in precedenza relativamente all'uso dellìoperatore [ ] per assegnare un elemento interno di un vettore ad una variabile.
Torniamo alla definizione di un vettore e ampliamo la gamma delle possibilità. Ovvero vediamo come creare un vettore a partire da altre sequenze.
1) da array a vettore (e viceversa)
è il passaggio più naturale,
necessario se volete passare da una struttura statica ad una dinamica.
Esiste una funzione molto comoda e dal nome che non lascia adito a dubbi
sulla sua funzionalità:
to_vec()
fn main() {
let array = [1, 2, 3, 4, 5];
let vect = array.to_vec();
println!("Vettore da array: {:?}", vect);
}
Molto semplice ma con qualche osservazione necessaria:
- gli elementi devono supportare Clone() perchè in effetti, internamente, questa è l'operazione svolta da to_vec(). Per gli elementi standard va tutto bene per quelli magari creati custom potrebbero esserci dei problemi ed è quindi necessario implmentare il supporto a Clone()
- quella che viene effettuata è una copia a tutti gli effetti quindi non è a costo zero.
detto questo, se l'array di partenza non serve più si può usare
Vec::from(array) che sposta gli elementi
dall'array al vettore. Finita l'operazione però l'array non è più
disponibile in quanto i suoi elementi sono stati mossi nel vettore.
fn main() {
let array = [1, 2, 3, 4, 5];
let vect =
Vec::from(array);
println!("Vettore da array: {:?}", vect);}
Nel paragrafo dedicato agli array abbiamo detto che il passaggio da vettore verso array non è così banale. Il motivo è sempre lo stesso, garantire che le dimensioni tra sorgente (vettore) e destinazione (array) siano uguali e tenere presente che la conversione può fallire. Vediamo quindi in dettaglio la faccenda iniziando dal caso più semplice, dove va tutto liscio:
use std::convert::TryInto;
fn main() {
let v: Vec<i32> = vec![1, 2,
3, 4];
let a: [i32; 4] = v.try_into().unwrap();
println!("{:?}", a);
}
qui siamo sicuri che le dimensioni coincidano e non ci sono problemi di sorta. Abbiamo usato try_into() che troviamo nel trait convert da includere esplicitamente. In realtà try_into() tenta la conversione e restituisce un Result (vedremo nel paragrafo apposito): se la lunghezza del Vec non corrisponde esattamente alla dimensione dell'array richiesta, ottieniamo un Err invece di un panic diretto (a differenza di altre operazioni con array/slice). Ecco perché serve gestire il risultato. In generale la forma migliore per impostare la cosa è definire l'array come segue:
(let a: [T; N] = ...) ovvero ad esempio
let array: [i32, 5]
come nel codice precedente. In breve, il seguente esempio dovrebbe fungere un po' da "strada mestra" per gestire questo caso:
esempio 22.5
use std::convert::TryInto;
fn main() {
let v: Vec = vec![1, 2, 3, 4];
let a: [i32; 3] = match v.try_into() {
Ok(arr) => arr,
Err(v) => {
println!("Conversione fallita, lunghezza errata: {}", v.len());
panic!("dimensione non corrispondente");
}
};
println!("{:?}", a);
}
che sarà più chiaro quando appunto avremo visto Result e le sue applicazioni. In queto modo, specificando dimensioni e tipo dell'array non avremo problemi a gestire situazioni di errore che dovessero presentarsi.
2) da slice a vettore
valgono le stesse modalità viste per gli array
con una differenza sostanziale, vediamo prima il codice:
fn main() {
let s1: &[i32] = &[1, 2, 3, 4];
let v1: Vec<i32> =
s1.to_vec();
let s2: &[i32] = &[5, 6, 7, 8];
let v2: Vec<i32> =
Vec::from(s2);
println!("{:?}", v1);
println!("{:?}", v2);
}
La differenza sta nel fatto che, stanto la natura degli slice, il tipo
dello slice deve supportare Clone perchè questo entra in gioco anche con
From, ovvero non vi è operazione di move verso il vettore.
Vi sono altri
modi ma questi sono più che sufficienti per moltissimi scopi.
3) da range a vettore
qui la cosa si fa ancora più
interessante anche a livello sintattico. Vediamo il programma e poi lo
commentiamo:
fn main() {
let range = 0..6; // Crea un range da 0 a 5
let vect =
range.collect::<Vec<_>>();
println!("Vettore da range: {:?}", vect);
}
quindi usiamo collect() che è un
eccellente metodo generico che può trasformare un iteratore in molti tipi
diversi, per cui lo vedremo all'opera molto spesso. Tuttavia passare al
compilatore un'istruzione tipo:let x = range.collect();
non sarebbe
sufficiente per far capire dove vogliamo andare a riconfigurare il nostro
range per cui dobbiamo aggiungere informazioni. Ci sono varie possibilità:
let range = 0..6;qui forniamo una chiara informazione della destinazione del range. E' un metodo molto idiomatico ma soprattutto molto leggibile.
let vect: Vec<i32> = range.collect();let range = 0..6;
let vect: Vec<_> = range.collect();
è simile al caso precedente ma qui lasciamo fare all'inferenza non specificando il tipo, ci limitiamo ad indicare al compilatore che la destinazione del range deve essere un vettore. Anche questo è un sistema molto elegante.- Il terzo metodo vede la comparsa del "turbofish" e si ha con la
seguente annotazione:
let range = 0..6; // Crea un range da 0 a 5
let vect = range.collect::<Vec<_>>();
La sintassi::<elemento>è nota appunto come "turbofish" per via della sua forma... (va beh...) ed è un aiuto che diamo all'inferenza per determinare su cosa deve lavorare. Fornisce quindi pieno controllo al programmatore sul tipo da utilizzare. In aggiunta abbiamo Vec<-> con l'elemento underscore all'interno della coppia <> che è un segnaposto per indicare che all'interno vi sarà un tipo di elemento che il compilatore potrà determinare da sè, ma questo è già noto. Dal momento che le forme precedenti sono decisamente più leggibili, questa opzione si adopera principalmente quando non dobbiamo assegnare una variabile ma passare direttamente ad una funzione il risultato della conversione, ad esempio dovendo passare un parametro direttamente ad una funzione e non si vuole prima creare una variabile su cui appoggiare la conversione, si potrebbe scrivere (qui abbiamo una ipotetica funzione di stampa):stampa(range.collect::<Vec<_>>());
- L'unico caso in cui
range.collect()può essere usato senza specifiche ulteriori è laddove il contesto renda palese l'attribuzione del tipo. Ho trovato online, non ricordo dove, il seguente frammento di codice che siega perfettamente questa situazione:fn stampa_vettore(v: Vec<i32>) {
println!("{:?}", v);
}
let range = 0..6;
stampa_vettore(range.collect());
In questo caso il tipo del parametro richiesto dalla funzione stampa basta a disambiguare.
4) da ennupla a vettore
L'ultimo caso che analizziamo
è il passaggio da ennupla a vettore. Qui bisogna fare attenzione perchè una
ennupla può contenere elementi di tipi diversi, un vettore no è un caso del
tutto simile.a quanto visto per gli array. Anche qui bisogna fare "a mano",
come nell'esempio che segue:
fn main() {
let tupla = (1, "due", 3.0);
let vect =
vec![tupla.0.to_string(), tupla.1.to_string(), tupla.2.to_string()];println!("Vettore da tupla: {:?}", vect);
}
che converte tutto verso il tipo stringa. Dovendo usare un tipo diverso bisogna usare la giusta funzione di conversione verso di esso, ovviamente bisogna accertarsi prima che gli elementi della ennupla possano essere riversati nel tipo designato per il vettore. Non esistendo un metodo automatico di conversione credo che questa sia l'unica strada, non molto efficiente ma non di strada prestazionalmente migliore mi pare non ce ne siani. In alternativa si potrebbe usare la macro format! che fa lo stesso mestiere, in modo forse più elegante
fn main() {
let tupla = (1, "due", 3.0);
let vect
= vec![
format!("{}", tupla.0),
format!("{}", tupla.1),
format!("{}", tupla.2),
];
println!("Vettore da tupla: {:?}", vect);
}
di format! dovremo parlare a parte perchè è interessante.
Vedremo più avanti un metodo piuttosto avanzato nel caso in cui la conversione non fosse possibile ma avessimo bisogno proprio di un vettore composto da elementi aventi tipologia diversa l'uno dall'altro.
Intanto torniamo su qualche istruzione utile per la manipolazione dei
nostri vettori.
La prima istruzione che analizziamo è
resize() che, come intuibile, serve a
modificare (in più o in meno) le dimensioni del vettore.
fn main() {
let mut v1 = vec![1,2,3];
v1.resize(5, 9);
println!("{:?}", v1);
v1.resize(1,0);
println!("{:?}", v1);
}
-- il primo parametro definisce la nuova lunghezza del vettore
-- il
secondo quali elementi aggiungere ma entra in gioco solo quando il vettore
cresce di dimensione, diversamente viene ignorato.
Stando così le cose
l'output è:
| [1, 2, 3, 9, 9] [1] |
abbiamo anche extend() che accoda elementi di un array o un altro vettore ma in generale di tutto ciò che implementa IntoIterator, quindi non una ennupla:
fn main() {
let mut v1 = vec![1,2,3];
v1.extend([55, 95]);
println!("{:?}", v1);
let v2 = vec![4, 5, 6];
v1.extend(v2); //v2
viene consumato se non clonato
println!("{:?}", v1);
}
con output:
| [1, 2, 3, 55, 95] [1, 2, 3, 55, 95, 4, 5, 6] |
Si tratta di una versione potenziata di push() in un certo senso ma consuma anche l'elemento aggiunto, a meno che questo non sia passato, se possibile, con .clone() come si vede anche dal commento nel codice precedente.
Scopo simile ha append(vettore) che però funziona solo per unire vettore con vettore, sono escluse altre tipologie di sequenze. Inoltre append non consuma il vettore in aggiunta la svuota lasciandolo con len pari a zero ma comunque ancora utilizzabile.
fn main() {
let mut vect1 = vec![1, 2, 3];
let mut vect2 = vec![4,
5, 6];
// append aggiunge v1 a v2
vect2.append(&mut vect1);
// vect1 adesso vuoto
println!("vect1: {:?}", vect1);
println!("vect2: {:?}", vect2);
}
Se usare extend o append dipende dal queste differenze, forse, append è più efficiente in termini prestazionali ma non ho fatto test ed è una considerazione che deve essere verificata.
A proposito di prestazioni, anche
with_capacity(s:usize) vi potrà aiutare ad ottenere performance
stabili predefinendo lo spazio da riservare ai vostri vettori:let
mut vec = Vec::with_capacity(10);
L'unico rischio è lo spreco
di memoria se esagerate concedendo capacità non necessarie.
clear() ripulisce il vettore e is_empty() ci può conferma il successo di quella operazione
fn main() {
let mut v1 = vec![1,2,3];
v1.clear();
println!("{}",
v1.is_empty());
}
Utile può essere verificare l'esistenza di un elemento in un vettore e determinarne la poszione, ovvero l'indice. Si usa contains(elemento) e una proprietà degli iteratori, vediamo l'esempio che si fa prima:
Esempio 22.6
fn main() {
let v = vec![1, 2, 3, 4, 5];
println!("Esiste 4? {}", v.contains(&4));
let i = v.iter().position(|&x| x == 20);
println!("Indice: {:?}", i);
let j = v.iter().position(|&x| x == 4);
println!("Indice: {:?}", j);
}
che ha questo output (lo so, non è chiaro, ma lo vedremo, Rust va preso a piccole dosi un po' qua un po' là)
| Esiste 4? true Indice: None Indice: Some(3) |
Utili possono essere:
v.sort() ordina in senso crescente
v.sort_by(|a, b| b.cmp(a)); in
senso inverso
ma bisogna fare attenzione, il modo corretto è questo:
fn main() {
let mut v0 = vec![10, 3, 45, 2, 3, 11];
v0.sort();
println!("{:?}", v0); // [2, 3, 3, 10, 11, 45]
v0.sort_by(|a, b|
b.cmp(a));
println!("{:?}", v0); // [45, 11, 10, 3, 3, 2]
}
non ad esempio:
let v1 = v0.sort();
Questi sono solo alcuni esempi, utili per lavorare da subito ma di metodi da utilizzare ce ne sono davvero molti, sul sito ufficiale li trovate ovviamente tutti e consiglio di dare uno sguardo a come sono costruiti questi metodi.
Un argomento
interessante è certamente la questione che riguarda l'operazione di copia di
un vettore.
La prima importante considerazione è che in Rust,
copiare un Vec è sempre concettualmente una "deep copy" e
non esiste un vero equivalente di shallow copy come in altri linguaggi (es.
Python con liste di riferimenti).
Altro punto da tenere presente è che
la copia con l'operatore =
non copia, effettua una move in quanto vec non implementa
Copy. Quindi:
let v1 = vec![1, 2, 3];
let v2 = v1; // MOVE, non copia!
// v1 non
è più disponibile qui
In casi come questo per mantenere l'originale bisogna usare clone():
let v1 = vec![1, 2, 3];
ancora disponile
let v2 = v1.clone();
// v1
Bisogna però distinguere due casi, come avrete già intuito:
1) gli elementi del vettore supportano Copy, come nel codice qui sopra che prevede degli interi i32. Clonare ogni elemento equivale a una semplice copia bit-per-bit (una memcpy del buffer, essenzialmente). È un'operazione economica, perché gli i32 non hanno alcun dato "esterno" collegato e vivono interamente dentro il buffer del Vec stesso. Il risultato è una copia completamente indipendente e "deep" per costruzione naturale, ma anche a basso costo perché non ci sono sotto-allocazioni. Insomma è la situazione perfetta.
2) gli elementi supportano Clone ma non Copy, è il solito classico caso delle stringhe
fn main() {
let v1: Vec<String> = vec![String::from("a"),
String::from("b")];
let v2 = v1.clone();
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
}
Qui ogni String possiede a sua volta un buffer heap separato (per i
propri caratteri). .clone() sul Vec, perciò il compilatore procede a due
fasi successive:
-- alloca un nuovo buffer per il Vec stesso (contenente
le "intestazioni" delle String: puntatore, len, capacity)
-- per ogni
String, chiama .clone() anche su di essa, che a sua volta alloca un nuovo
buffer heap per il testo
La copia è sempre prodonfa ma, come possiamo facilmente immaginare, è anche più dispendiosa.
3) gli elementi non supportano Clone() (e quindi neanche Copy)
qui si fa dura, si deve ricorrere a =, che
come abbiamo visto cosuma il vettore origine o a dei riferimenti tramite
&.
E se ci servisse una shallow-copy? Sostanzialmente abbiamo due metodi, uno basico, che fa uso di una condivisione tramite puntatore:
let v1 = vec![1, 2, 3];
let v2 = &v1; // v2 è un riferimento, non una
copia indipendente
e l'altro di cui presento solo un frammento di codice perchè coinvolge concetti che vedremo molto più avanti relativi ai thread e che risulta essere molto efficiente
use std::rc::Rc;
let v1 = Rc::new(vec![1, 2, 3]);
let v2 =
Rc::clone(&v1); // clona solo il puntatore + incrementa un contatore, NON i
dati!
println!("{:?} {:?}", v1, v2); // stesso buffer condiviso
Questo è certamente molto bello e, come suol dirsi, idiomatico ma a questo punto del nostro percorso è quasi incomprensibile.
Un'ultima chicca in questo ambito può essere data da clone_from() che ha la particolarità di poter utilizzare un buffer esistente. E' interessante anche perchè non è specifico per i vec. Fornisco un esempio di codice, potete adattarlo anche per altre sequenze:
Esempio 22.7
fn main() {
let v1: Vec = vec![1, 2, 3];
let mut v2: Vec = vec![10, 20, 30, 40, 50];
let mut v3: Vec = vec![100, 200, 300, 400, 500];
// Con clone() classico:
// alloca un NUOVO buffer, poi scarta il vecchio buffer di v2
v2 = v1.clone();
println!("v2: {:?}", v2); // v2: [1, 2, 3]
// Con clone_from():
// riutilizza il buffer ESISTENTE di v3, se capacità sufficiente
v3.clone_from(&v1);
println!("v3: {:?}", v3); // v3: [1, 2, 3]
}
Se la destinazione è più piccola dell'originale viene sfruttato lo spazio esistente e poi aggiunto quello mancante. Il vantaggio è chiaramente quello di poter usare spazio esistente già in uso, in tutti i casi.
Un'altra situazione interessante che possiamo illustrare grazie ai vect è la seguente, esempio classico presente anche sulla documentazione ufficiale che ci permette di disquisire un po' sulle caratteristiche e sulla filosofia del compilatore:
Esempio 22.8
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
Questo programma non compila e abbiamo un bel:
error[E0502]: cannot borrow `v` as
mutable because it is also borrowed as immutable
--> r343.rs:4:5
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("The first element is: {first}");
| ----- immutable borrow later used here
error: aborting due to 1 previous error
però se eliminiamo o riduciamo a commento la riga relativa alla stampa invece va tutto bene. Ma come? Se abbiamo un borrow immutabile e poi cerchiamo di modificarlo? Stampa o non stampa se aggiungiamo un elemento il vettore potrebbe dover essere riallocato e allora first diventerebbe un dangling pointer. Qui entra in ballo un concetto che sarà più chiaro più avanti ma che possiamo introdurre qui ovvero si parla Non Lexical Lifetime (NLL). In pratica Il punto chiave è che il prestito dura solo fino all'ultimo uso, non fino alla fine dello scope. Quindi se c'è la richiesta di stampa il compilatore considera ancora in uso quel prestito ed allora ecco l'errore. Se rimuoviamo la println!, il compilatore si accorge che first non viene mai più usato dopo la sua dichiarazione e quindi, grazie alle NLL, considera il prestito già terminato subito dopo l'ultima riga in cui first compare (cioè la sua creazione stessa, dato che non viene mai riletto). Il prestito quindi "muore" prima della push(), e non c'è più conflitto. Il borrow checker analizza tutto il flusso del programma per decidere se un'operazione è sicura, non solo la singola riga. Se un riferimento non verrà mai più letto, non c'è alcun pericolo che punti a memoria invalidata, quindi non c'è motivo di proibire la mutazione successiva. Fino a prima del 2018 il compilatore bloccava il processo anche in assenza dell'istruzione di stampa poi è stata introdotta questa modifica che ha reso meno pedante (così dicono) il suo compotamento. Diciamo che in questo modo passa per buono codice "sospetto" dal punto di vista della logica di programmazione ma innocuo in termini di conseguenze. C'è stata anche una discussione sul forum di Rust dove è stato spiegato in pratica che il compilatore, senza perdonare codice pericoloso, lascia più libertà così. Prendiamo per buona questa cosa.
VETTORI MULTI TIPO - (Argomento avanzato)
Questo paragrafo per ora si può saltare e ritornare su di esso più avanti.
Eccoi davanti ad un rgomento interessante. Come abbiamo detto i vettori sono composti da elementi tutti dello stesso tipo. Ma non è proprio possibile aggirare questa cosa? In realtà si in un paio di modi diversi ma noi ne useremo uno solo, più chiaro e sicuro. Questo metodo si basa su un enumeratore come vediamo nel seguente esempio che è realmente indicativo di come lavorare in maniera sicura:
Esempio 22.9
enum Valore {
Carattere(char),
Intero(i32),
Testo(&'static str),
Decimale(f64),
}
fn main() {
let arr: Vec = vec![
Valore::Carattere('a'),
Valore::Intero(1),
Valore::Testo("hello"),
Valore::Decimale(2.1),
];
let elemento = &arr[1];
print!("Elemento: ");
match elemento {
Valore::Carattere(c) => println!("Carattere: {}", c),
Valore::Intero(i) => println!("Intero: {}", i),
Valore::Testo(s) => println!("Testo: {}", s),
Valore::Decimale(d) => println!("Decimale: {}", d),
}
}
L'aggiunta di un elemento comporta la modifica dell'enumeratore e di
tutti i match che coinvolgono il vettore, questo è un vantaggio, pur
appesantendo un po' la fase di manutenzione, in quanto evita pericolosi bug.
L'altro metodo fa uso di Any che è un trait speciale che permette il
downcasting a runtime cioè possiamo "chiedere" a un valore se è di un
certo tipo concreto, e in caso positivo recuperarlo. Questo sistema è molto
comodo in quanto permette anche di modificare il vettore se dovete
aggiungere o togliere elementi senza altri problemi:
Esempio 22.10
use std::any::Any;
fn main() {
let arr: Vec> = vec![
Box::new('a'),
Box::new(1),
Box::new("hello"),
Box::new(2.1f64),
];
println!("arr: {}", arr[1].downcast_ref::<i32>().unwrap());
}
Ci sono svariati problemi però:
1) Perdiamo completamente la sicurezza dei tipi a
compile-time: il compilatore non sa più cosa contiene ogni elemento, quindi
ogni accesso richiede un downcast con gestione esplicita del fallimento
(Option) (basta provare ad esempio a sostituire nel 22.10 il tipo i32 con
f32 e vedere cosa succede a runtime)
2) Dobbiamo provare
"a tentoni" ogni tipo possibile con una catena di if let/match, e se
dimentichiamo un caso, semplicemente non gestiamo quel valore (bug
silenzioso, non errore di compilazione)
3) Overhead a
runtime: ogni downcast_ref fa un controllo del tipo a runtime (confronto di
TypeId)
4) Doppia indirection: Box alloca sull'heap, e
dyn Any aggiunge dispatch dinamico
5) Non è estensibile
in modo sicuro (anche se semplice come abbiamo detto): se qualcun altro
aggiunge un nuovo tipo al vettore, nessun errore di compilazione avvisa che
il codice di gestione non copre quel caso
Questo argomento non è banale, come si vede e coinvolge molti altri concetti per cui prendetelo così com'è.
Con i vettori abbiamo chiuso, per ora ma torneremo a vederli in azione da protagonisti quando parleremo delle funzioni.