Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 047 – Elaborazione di una serie di elementi con gli iteratori

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Corso su Elaborazione di una Serie di Elementi con Iteratori

Il modello dell’iteratore ti permette di eseguire alcune operazioni su una sequenza di elementi uno alla volta. Un iteratore è responsabile della logica di iterazione su ciascun elemento e di determinare quando la sequenza è terminata. Quando si usano gli iteratori, non è necessario reimplementare questa logica da soli.

In Rust, gli iteratori sono pigri, il che significa che non hanno alcun effetto fino a quando non si chiamano metodi che consumano l’iteratore per utilizzarlo fino in fondo. Ad esempio, il codice nel Listato 13-10 crea un iteratore sugli elementi del vettore v1 chiamando il metodo iter definito su Vec<T>. Questo codice di per sé non fa nulla di utile.

rust

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

Il risultato dell’iteratore è memorizzato nella variabile v1_iter. Una volta creato un iteratore, possiamo usarlo in vari modi. Nel Listato 3-5 nel Capitolo 3, abbiamo iterato su un array usando un ciclo for per eseguire del codice su ciascuno dei suoi elementi. In fondo, ciò ha implicitamente creato e quindi consumato un iteratore, ma abbiamo tralasciato come funziona esattamente fino ad ora.

Nell’esempio nel Listato 13-11, separiamo la creazione dell’iteratore dall’uso dell’iteratore nel ciclo for. Quando il ciclo for viene chiamato usando l’iteratore in v1_iter, ogni elemento nell’iteratore viene utilizzato in una iterazione del ciclo, stampando ogni valore.

rust

let v1 = vec![1, 2, 3]; let v1_iter = v1.iter();

for val in v1_iter {
println!("Ottenuto: {}", val);
}

Nel linguaggi che non hanno iteratori forniti dalle loro librerie standard, probabilmente scriveresti questa stessa funzionalità iniziando una variabile a indice 0, usando quella variabile per indicizzare il vettore per ottenere un valore e incrementando il valore della variabile in un ciclo finché non raggiungi il numero totale di elementi nel vettore.

Gli iteratori gestiscono tutta quella logica per te, riducendo il codice ripetitivo che potresti potenzialmente confondere. Gli iteratori ti danno più flessibilità nell’uso della stessa logica con molti tipi diversi di sequenze, non solo strutture dati in cui puoi indicizzare, come i vettori. Esaminiamo come gli iteratori facciano ciò. Il Trait Iterator e il Metodo next

Tutti gli iteratori implementano un trait chiamato Iterator che è definito nella libreria standard. La definizione del trait è la seguente:

rust

pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;

// metodi con implementazioni predefinite omesse
}

Nota che questa definizione utilizza una nuova sintassi: type Item e Self::Item, che definiscono un tipo associato a questo trait. Parleremo dei tipi associati in dettaglio nel Capitolo 19. Per ora, tutto ciò che devi sapere è che questo codice dice che implementare il trait Iterator richiede anche che tu definisca un tipo Item, e questo tipo Item è usato nel tipo di ritorno del metodo next. In altre parole, il tipo Item sarà il tipo restituito dall’iteratore.

Il trait Iterator richiede solo agli implementatori di definire un metodo: il metodo next, che restituisce un elemento dell’iteratore alla volta incapsulato in Some e, quando l’iterazione è finita, restituisce None.

Possiamo chiamare il metodo next sugli iteratori direttamente; il Listato 13-12 dimostra quali valori vengono restituiti dalle chiamate ripetute a next sull’iteratore creato dal vettore.

rust

#[test]
fn dimostrazione_iteratore() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();

assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}

Nota che abbiamo dovuto rendere v1_iter mutabile: chiamare il metodo next su un iteratore cambia lo stato interno che l’iteratore usa per tenere traccia di dove si trova nella sequenza. In altre parole, questo codice consuma, o utilizza, l’iteratore. Ogni chiamata a next consuma un elemento dall’iteratore. Non abbiamo dovuto rendere v1_iter mutabile quando abbiamo usato un ciclo for perché il ciclo ha preso il possesso di v1_iter e l’ha reso mutabile dietro le quinte.

Nota anche che i valori che otteniamo dalle chiamate a next sono riferimenti immutabili ai valori nel vettore. Il metodo iter produce un iteratore su riferimenti immutabili. Se vogliamo creare un iteratore che prende la proprietà di v1 e restituisce valori posseduti, possiamo chiamare into_iter invece di iter. Allo stesso modo, se vogliamo iterare su riferimenti mutabili, possiamo chiamare iter_mut invece di iter. Metodi che Consumano l’Iteratore

Il trait Iterator ha diversi metodi con implementazioni predefinite fornite dalla libreria standard; puoi scoprire questi metodi consultando la documentazione dell’API della libreria standard per il trait Iterator. Alcuni di questi metodi chiamano il metodo next nella loro definizione, motivo per cui è necessario implementare il metodo next quando si implementa il trait Iterator.

I metodi che chiamano next sono chiamati adattatori consumatori, perché chiamarli utilizza l’iteratore. Un esempio è il metodo sum, che prende la proprietà dell’iteratore e itera attraverso gli elementi chiamando ripetutamente next, consumando così l’iteratore. Mentre itera, aggiunge ciascun elemento a un totale parziale e restituisce il totale quando l’iterazione è completata. Il Listato 13-13 ha un test che illustra un utilizzo del metodo sum:

rust

#[test]
fn somma_iteratore() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); let totale: i32 = v1_iter.sum();

assert_eq!(totale, 6);
}

Non ci è permesso utilizzare v1_iter dopo la chiamata a sum perché sum prende la proprietà dell’iteratore su cui lo chiamiamo. Metodi che Producono Altri Iteratori

Gli adattatori di iteratori sono metodi definiti sul trait Iterator che non consumano l’iteratore. Invece, producono diversi iteratori cambiando qualche aspetto dell’iteratore originale.

Il Listato 13-14 mostra un esempio di chiamata al metodo adattatore di iteratori map, che prende una closure da chiamare su ciascun elemento mentre gli elementi vengono iterati. Il metodo map restituisce un nuovo iteratore che produce gli elementi modificati. La closure qui crea un nuovo iteratore in cui ciascun elemento del vettore verrà incrementato di 1:

rust

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

Tuttavia, questo codice produce un avviso:

less

$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default

warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`

Il codice nel Listato 13-14 non fa nulla; la closure che abbiamo specificato non viene mai chiamata. L’avviso ci ricorda perché: gli adattatori di iteratori sono pigri, e qui dobbiamo consumare l’iteratore.

Per risolvere questo avviso e consumare l’iteratore, useremo il metodo collect, che abbiamo usato nel Capitolo 12 con env::args nel Listato 12-1. Questo metodo consuma l’iteratore e raccoglie i valori risultanti in un tipo di dati di raccolta.

Nel Listato 13-15, raccogliamo i risultati dell’iterazione sull’iteratore restituito dalla chiamata a map in un vettore. Questo vettore conterrà ogni elemento del vettore originale incrementato di 1.

rust

let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

Poiché map prende una closure, possiamo specificare qualsiasi operazione vogliamo eseguire su ciascun elemento. Questo è un ottimo esempio di come le closure ti consentano di personalizzare un certo comportamento mentre riutilizzi il comportamento di iterazione fornito dal trait Iterator.

Puoi concatenare più chiamate agli adattatori di iteratori per eseguire azioni complesse in modo leggibile. Ma poiché tutti gli iteratori sono pigri, devi chiamare uno dei metodi adattatori consumatori per ottenere risultati dalle chiamate agli adattatori di iteratori. Utilizzo delle Closure che Catturano il Proprio Ambiente

Molti adattatori di iteratori accettano chiusure come argomenti, e comunemente le chiusure che specificheremo come argomenti agli adattatori di iteratori saranno chiusure che catturano il proprio ambiente.

Per questo esempio, useremo il metodo filter che prende una closure. La closure riceve un elemento dall’iteratore e restituisce un bool. Se la closure restituisce true, il valore sarà incluso nell’iterazione prodotta da filter. Se la closure restituisce false, il valore non sarà incluso.

Nel Listato 13-16, usiamo filter con una chiusura che cattura la variabile shoe_size dal suo ambiente per iterare su una collezione di istanze di strutture Shoe. Restituirà solo scarpe della dimensione specificata.

rust

#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
} #[cfg(test)]
mod tests {
use super::*; #[test]
fn filtra_per_dimensione() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
]; let in_my_size = shoes_in_size(shoes, 10);

assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}

La funzione shoes_in_size prende la proprietà di un vettore di scarpe e una dimensione di scarpa come parametri. Restituisce un vettore contenente solo le scarpe della dimensione specificata.

Nel corpo di shoes_in_size, chiamiamo into_iter per creare un iteratore che prende la proprietà del vettore. Quindi chiamiamo filter per adattare quell’iteratore in un nuovo iteratore che contiene solo gli elementi per i quali la chiusura restituisce true.

La chiusura cattura il parametro shoe_size dall’ambiente e confronta il valore con la dimensione di ogni scarpa, mantenendo solo le scarpe della dimensione specificata. Infine, chiamare collect raccoglie i valori restituiti dall’iteratore adattato in un vettore che viene restituito dalla funzione.

Il test mostra che quando chiamiamo shoes_in_size, otteniamo solo le scarpe che hanno la stessa dimensione del valore specificato.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *