Rust - ciclo for
Le iterazioni introdotte dal for sono decisamente più varie di quelle viste
finora. Anche la sintassi quindi cambia a seconda della modalità con la quale
intendiamo utilizzare questo costrutto. La sua versatilità lo rende tra gli
strumenti più utili e certamente lo userete molto spesso nei vostri programmi.
Tra l'altro vi è una particolarità che lo rende unico rispetto ad analoga
istruzione in altri linguaggi, in pratica c'è qualosa di più del semplice
iterare in un range.
Partiamo comunque dalle fondamenta ovvero dalla sintassi
di base:
for variabile in espressione {
istruzioni
}
Un concetto che si accompagna spesso il nostro for
è quello, appena menzionato, di range ai quali
accenniamo brevemente, ad essi dedicheremo comunque un paragrafo specifico. Un
range tipicamente ha il formato
start..end
che equivale,
supponendo di ispezionare tale range con la variabile x a:start <= x <
end
quindi escluso il valore più alto. Se vogliamo che anche questo sia
incluso la sintassi diventa:start ..= end
possiamo quindi partire
con il primo semplice esempio:
Esempio 21.1
fn main(){
for _ in 0..5
{
println!("ciao");
}
}
Ricordiamo che la variabile contrassegnata con il segnaposto _ ha
un valore che può essere ignorato ed evita il warning da parte del
compilatore. Questo programma espone a video la stringa "ciao"
(senza apici, ovviamente) per 5 volte (da 0 compreso a 4). Va detto che se
al posto di _ mettessimo una x questa non sarebbe utilizzabile
esternamente al ciclo, per le solite ragioni di ambito che
dovrebbero essere chiare a questo punto.
Anche se non direttamente
collegato in maniera peculiare all'istruzione for bisogna fare un po' di
attenzione perchè le regole dell'ownership sono sempre in agguato e visto
che le iterazioni su range sono il pane quotidiano vale la pena perderci
qualche minuto. Vediamo l'esempio seguente:
Esempio 21.2
fn main(){
let r1 = 0..10;
for x in r1
{
println!("{}", x);
}
println!("{}", r1.len());
}
Questo programma non compila e il messaggio d'errore è molto chiaro, come sempre, ma ci introduce anche ad uno scenario più profondo del nostro ciclo for:
error[E0382]: borrow of moved
value: `r1`
--> r273.rs:7:20
|
2 | let r1 = 0..10;
| -- move occurs because `r1` has
type `std::ops::Range<i32>`, which does not implement the `Copy` trait
3 | for x in r1
| -- `r1` moved due to this
implicit call to `.into_iter()`
...
7 | println!("{}", r1.len());
| ^^ value borrowed here after
move
|
note: `into_iter` takes ownership
of the receiver `self`, which moves `r1`
|
312 | fn into_iter(self) ->
Self::IntoIter;
| ^^^^
In pratica il compilatore ci dice che il range non supporta il trait Copy e pertanto viene "attratto" all'interno del for e quindi non può essere utilizzato dalla macro di stampa. E' un meccanismo che conosciamo ma in questo caso c'è un concetto in più che è anche quello che caratterizza il for in Rust e lo vediamo nella riga evidenziata in arancione. In pratica una istruzione for del tipo
for x in collezione {
....
}
equivale a qualcosa del tipo:
let mut iteratore = collection.into_iter();
loop {
match
iteratore.next() {
Some(x) => {
// ...
}
None => break,
}
}
In pratica quello che viene passato è un "qualcosa" che può essere trasformato in un iteratore tramite IntoIterator che è il trait che definisce la conversione di un tipo in un iteratore. È il meccanismo che permette a un tipo di essere usato in un costrutto for o in metodi come into_iter(), for_each(), ecc. Quindi, alla fine:
for x in qualcosa
significa:
Trasforma qualcosa in un iteratore e dammi
progressivamente i valori prodotti da quell'iteratore.
Il punto
chiave è che quando trasformiamo il qualcosa in un iteratore ne prendiamo il
possesso trascinandolo all'interno del for
per cui, quando questo termina, anche il qualcosa esce dallo scope insieme
al for. In questo senso Rust ci permette
però di ragionare in varie modalità. Ovvero data una sequenza,
lavorando sui suoi elementi non ci limitiamo a
scorrerli nel corso dell'iterazione ma possiamo:
- consumarla
- leggerla
- modificarla
Vediamo i 3 casi prendendo come sequenza un vettore (dei quali non
abbiamo ancora parlato ma, per i nostri scopi in questo capitolo, sono delle
semplici sequenze di elementi):let v =
vec![10,20,30]
consumiamo la sequenza:for x in v {
println!("{x}");
}
In questo caso il vettore v viene catturato all'interno del ciclo aperto con for e non è più disponibile all'esterno, ovvero una volta che il ciclo sia chiuso
fn main(){
let v = vec![10,20,30];
for x in v {
println!("{x}");
}
// println!("{:?}", v);
}
questo codice funziona
solo se si mantiene il commento sull'ultima istruzione di stampa altrimenti
abbiamo un errore.error[E0382]: borrow of moved value: `v`
--> r275.rs:6:22
2 | let v = vec![10,20,30];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | for x in v {
| - `v` moved due to this implicit call to `.into_iter()`
...
6 | println!("{:?}", v);
| ^ value borrowed here after move
prestito immutabile, quindi siamo in modalità di lettura:
for x in &v {
*x += 10;
}
questo non funziona. L'errore è chiarissimo:
error[E0594]: cannot assign to
`*x`, which is behind a `&` reference
--> r276.rs:4:3
3 | for x in &v {
| -- this iterator yields `&` references
4 | *x += 10;
| ^^^^^^^^ `x` is a `&` reference, so it cannot be
written to
help: use
a mutable iterator instead
3 | for x in &mut v {
e le righe in rosso segnalano la soluzione. Il fatto è che puntiamo ad un vettore e ai suoi elementi ma questi non sono modificabili. Quindi, come detto, nioente scrittura. Leggere però si può:
fn main(){
let v = vec![10,20,30];
for x in &v {
println!("{}",
x);
}
}
prestito mutabile - anche scrittura
In questo caso, come avrete intuito entra in gioco la keyword mut:
fn main(){
let mut v = vec![10,20,30];
for x in &mut v {
*x +=
100;
}
println!("{:?}", v);
}
e stavolta ci siamo. Come è facile notare dall'output
| [110, 120, 130] |
abbiamo moltiplicato per un fattore 10 gli elementi del vettore. Vedremo più avanti in questo stesso paragrafo anche la creazione di un iteratore come via alternativa di manipolazione.
Un'altra strategia per effettuare delle modifiche ma lasciare intatto il vettore (o qualunque sequenza usiate) oppure lavorare tramite esso senza incorrere nel problema del move è quella di usare clone() ovvero creare una copia e poi lavorare su quella. Banale ma utile ricordarlo. Ad esempio:
fn main(){
let r1 = 0..10;
for x in r1.clone()
{
println!("{}", x);
}
println!("{}", r1.len());
}
Lavorare con il for è di solito abbastanza semplice, laddove come visto si tengano presenti le insidie del borrow checker. In fondo al paragrafo troverete un ulteriori approfondimento che potrete anche saltare.
Proseguiamo però adesso con alcune particolarità recniche che
possono venire utili in alcuni casi.
Ad esempio come possiamo fare
se
vogliamo lavorare con uno step diverso da 1? vediamo il seguente frammento
di codice:
for _x in (0..=10).step_by(2) {
println!("ciao");
}
abbiamo evidenziato la funzione step_by(n) che gestisce il salto di n elementi. Bisogna fare attenzione che se lavorate con sequenze diverse da un range potreste aver bisogno di qualche aggiustamento. Ad esempio con un vettore è necessario generare un iteratore:
fn main(){
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for x in
v.iter().step_by(2) {
println!("{}", x);
}
println!("{:?}",
v);
}
quel v.iter() non genera presa di possesso da parte del ciclo for per cui è un altro modo, quello a cui accennavamo in precedenza, per gestire un loop senza incorrere in problemi con il borrow checker. Questo perché v.iter() non prende possesso di v, ma ne restituisce un iteratore di riferimenti.
Se volete percorrere una sequenza al contrario la funzione che cercate è rev():
for x in (0..5).rev() per una sequenza o
for x in v.iter().rev()
con un vettore
Può essere interessante fare qualche prova con altre sequenze.
Anche nel caso del for valgono le consuente regole per break e continue viste in altri casi:
Esempio 21.3
fn main(){
for x in 0..10
{
if x == 3
{
continue;
}
println!("{}", x);
if x == 7
{
break;
}
}
}
con il salto del valore 3 e uscita quando viene incontrato il valore 7.
Un problema che si incontra a volte è quello di dover lavorare con più indici. Si può risolvere in vari modi, in generale se volete lavorare in maniera magari non idiomatica in senso stringente ma molto comoda e con una valenza generale forse conviene ricorrere a while:
Esempio 21.4
fn main() {
let (mut x, mut y) = (0, 0);
while x <= 10 && y <= 10 {
println!("{}", x + y);
x += 1;
y += 1;
}
}
Con questo metodo, molto semplice ed intuitivo, non si sbaglia mai, direi. Tuttavia è interessante in questa sede introdurre la funzione zip() che lavora direttamente su iteratori invece che su variabili. Il seguente esempio ottiene lo stesso risultato del codice del 21.4:
Esempio 21.5
fn main() {
for (x, y) in (0..=10).zip(0..=10) {
println!("{}", x + y);
}
}
Se gli iteratori fosse più di 2, dal momento che zip è operatore binario dovreste ricorrere a livelli di parentesi più estesi, col risultato che il codice diventerebbe più caotico al crescere degli elementi coinvolti, qui vediamo il caso di 3:
for ((x, y), z) in ((0..=10).zip(0..=10)).zip(0..=10)
Per casi come questi o più complessi invece di impazzire con i livelli di parentesi potete ricorrere al solito while con più variabili oppure al crate itertools (che è la via idiomatica per N iteratori) che però potete usare, se non volete scaricare il sorgente, tramite il tool cargo (al quale dedichiamo una sezione per comprenderlo almeno per sommi capi), come nell'esempio che segue che presento step-by-step:
1) cargo new itera (Itera è il nome che ho scelto, ovviamente è libero)
2) edito cargo.toml modificando la sezione delle dependencies:
[dependencies]
itertools = "0.15"
la 0.15 mi risulta essere l'ultima
versione di itertools, almeno così è nel momento in cui scrivo. A questo punto il codice del programma diventa:
use itertools::izip;
fn main() {
for (x, y, z) in izip!(0..=10,
0..=10, 0..=10) {
println!("{}", x + y + z);
}
}
Itertools, potrete eventualmente
analizzarlo dopo una semplice ricerca, non fa solo questo.
In caso di coppie correlate, caso che si presenta abbadtanza spesso,
potete usare le ennuple:
Esempio 21.6
fn main() {
let coppie = [(1, "uno"), (2, "due"), (3, "tre")];
for (numero, nome) in coppie.iter() {
println!("{}: {}", numero, nome);
}
}
da cui otteniamo:
| 1: uno 2: due 3: tre |
Sempre utilizzando una ennupla vediamo un semplice sistema per tenere traccia delle iterazioni e che fa uso del metodo enumerate(). Vediamo come funziona con il solito semplice esempio:
Esempio 21.7
fn main(){
for (i,j) in (5..9).enumerate(){
println!("i = {} e j = {}", i, j);
}
}
e qui abbiamo
| i = 0 e j = 5 i = 1 e j = 6 i = 2 e j = 7 i = 3 e j = 8 |
Prima di chiudere segnalo un uso un po' particolare di "etichette".
Proprio così, possiamo definire delle etichette che permettono di saltare da
un punto ad un altro di un ciclo. No, non è il goto che so che odiate tutti
(ma perchè poi...). Quindi esiste la possibilità di usare delle etichette
con break e continue in presenza di cicli annidati. Le etichette seguono la
sintassi:'nome-etichetta punto di richiamo dell'etichetta
'nome-etichetta:
punto di arrivo dell'etichetta
Se pensate che questa possibilità
avvicini il classico e famigerato goto presente in altri linguaggi, non è
questo il caso, vuoi per l'ambito limitato ai soli cicli for
while e loop,
vuoi per il controllo a cui il compilatore sottopone l'uso di queste
etichette. Vediamo quindi un esempio:
Esempio 21.8
fn main() {
'etik: for i in 1..=5 {
for j in 1..=5 {
if j == 3 {
println!("Salto se j == 3 (i: {}, j: {})", i, j);
continue 'etik; // Va alla prossima iterazione del ciclo esterno
}
println!("i: {}, j: {}", i, j);
}
}
}
Come si vede siamo concettualmente vicini alla logica del goto (e questo impedisce il raggiungimento di valori di j superiori a 3) ma la cosa non è proprio identica. Il goto classico può saltare ovunque, anche fuori da funzioni, dentro blocchi arbitrari, creando flussi di controllo difficili da seguire (da qui la fama negativa, uno dei documenti più famosi in merito è il celebre "Goto Considered Harmful" di Dijkstra). I label di Rust invece sono strutturalmente vincolati, li possiamo usare solo dentro for, while e loop, possiamo usarli solo per uscire e non per entrare in un livello più basso. Peraltro l'uso di queste etichette permette di semplificare la composizione di cicli come quello dell'esempio 21.8
ANCORA QUALCOSA SU MOVE
Ancora un esempio su questa faccenda un po' intricata (almeno per me) perchè, a mio avviso, trasmette veramente lo spirito di Rust. Prendiamo stavolta in esame un normale array:
fn main() {
let ar1 = [10, 20, 30];
for x in ar1 {
println!("{x}");
}
println!("{:?}", ar1);
}
In questo caso non c'è alcun move in quanto gli elementi all'interno dell'array implementano il trait Copy. Ora però vediamo se all'interno abbiamo elementi di natura diversa, delle stringhe, ad esempio:
fn main() {
let ar1 = [String::from("a"), String::from("b")];
for x in ar1 {
println!("{x}");
}
println!("{:?}", ar1);
}
questo programma non compila è il motivo lo potete intuire da soli ed è comunque espresso molto bene dall'errore che inizia come segue e che ci presenta come sempre la presenza di into_iter:
error[E0382]: borrow of moved
value: `ar1`
--> r296.rs:7:20
|
2 | let ar1 = [String::from("a"),
String::from("b")];
| --- move occurs
because `ar1` has type `[String; 2]`, which does not implement the `Copy`
trait
3 |
4 | for x in ar1 {
| --- `ar1` moved due to
this implicit call to `.into_iter()`
Una soluzione è quella di chiamare l'array per riferimento:
for x in &ar1
Direi che a questo punto c'è abbastanza materiale su questo importante ed interessante ciclo.