Quando dobbiamo accedere agli elementi di un tipo di dato complesso come vettori e liste in Rust, possiamo usare un iteratore, un oggetto che accede ad un dato alla volta contenuto al loro interno.
Gli iteratori hanno molti vantaggi rispetto ai cicli for. Ci permettono di avanzare quando vogliamo noi, di filtrare e modificare i dati contenuti nella lista. Prendendo come esempio lo sviluppo di un videogioco, possiamo avanzare nei dialoghi e andare avanti e indietro in una lista di azioni.
Uso degli iteratori in Rust
Possiamo usare gli iteratori in due modi: richiamando i metodi direttamente sugli oggetti che già li implementano oppure implementarli nelle nostre strutture. Nel primo caso, ci basta richiamare il metodo .iter() sulla variabile che contiene la collezione e poi usare gli altri metodi per filtrare o modificare i campi tramite le closure. Nel secondo caso implementiamo il trait Iterator sulla struct e diciamo al programma cosa fare quando richiamiamo .next().
Questo è l’esempio operando direttamente con i tipi che supportano gli iteratori. Il vantaggio è anche il codice più pulito e facile da leggere:
fn main() {
let num = vec![1, 2, 3];
let square_num: Vec<i32> = num.iter() //Creiamo l'iteratore
.map(|&n| n * n) //Prendiamo ogni elemento e calcoliamo il quadrato
.collect(); //Creiamo un nuovo vettore.
for n in &num {
println!("{}", n);
}
for n in &square_num {
println!("{}", n);
}
}
Quando implementiamo il trait Iterator nelle nostre Struct possiamo fare in modo che sia molto più facile e veloce scrivere il codice nei metodi che devono usarlo:
struct Counter {
count: i32,
}
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut counter = Counter{ count: 3 };
while let Some(n) = counter.next() {
println!("{}", n);
}
}
Iteratori o cicli
Per avere un’idea sulla differenza tra usare i cicli o gli iteratori, possiamo vedere il codice usato per creare il mingrep. La guida principale si trova nella documentazione di Rust ma ho modificato il codice in modo da avere anche le funzioni di ricerca tutte nella stessa struttura. Il codice completo è disponibile in GitHub. Qui vediamo soltanto la parte dove modifichiamo i cicli per inserire gli iteratori. Esempio con ciclo for:
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore_case})
}
.....
.....
pub fn search_query_by_contents(&self) -> Result<Vec<String>, Box<dyn Error>> {
let mut results = Vec::new();
let contents = self.get_contents()?;
for line in contents.lines() {
if line.contains(&self.query) {
results.push(line.to_string());
}
}
Ok(results)
}
}
Esempio gli iteratori:
impl Config {
pub fn build(mut args: impl Iterator<Item = String>,) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore_case})
}
....
....
pub fn search_query_by_contents(&self) -> Result<Vec<String>, Box<dyn Error>> {
let results = self.get_contents()?
.lines()
.filter(|line| line.contains(&self.query))
.map(|line| line.to_string())
.collect();
Ok(results)
}
}
Differenza in main:
//Prima:
let args: Vec<String> = env::args().collect();
let config = Config::build( &args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
//Dopo:
let config = Config::build( env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
L’uso di uno di questi metodi o della combinazione di entrambi dipende da ciò che bisogna fare e quale codice richiede meno lavoro per il PC. Tuttavia, in generale si preferisce lavorare con gli iteratori e le closure per avere un codice più pulito e chiaro.
Altri esempi con gli iteratori in Rust
Un altro esempio è legato ai videogiochi. Supponiamo di avere dei dialoghi dentro una collezione di frasi da dire. Se usassimo un ciclo ogni frase verrebbe letta subito, senza aspettare o permetterci di interagire. In questi casi gli iteratori sono molto utili:
use std::io; // Per leggere l'input dell'utente
fn main() {
let dialoghi = vec![
"Eroe: Chi sei?",
"NPC: Io sono il guardiano di questa città.",
"Eroe: Ho un dono per la principessa da parte del mio maestro.",
];
let mut iter = dialoghi.iter(); // Creiamo l'iteratore
loop {
println!("Premi 'a' e poi INVIO per avanzare nel dialogo, oppure 'q' per uscire.");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap(); // Legge l'input dell'utente
if input.trim() == "q" {
println!("Hai terminato la conversazione.");
break;
}
if input.trim() == "a" {
if let Some(riga) = iter.next() {
println!("{}", riga);
} else {
println!("Fine del dialogo.");
break;
}
}
}
}
Diverso è il caso che vogliamo dividere le funzioni da main e soprattutto quando vogliamo tornare indietro. In questo caso non possiamo usare un iteratore ma creare un indice e usarlo come riferimento per una condizione:
use std::io; // Import necessario per leggere l'input dell'utente
fn main() {
let dialoghi = vec![
"Eroe: Chi sei?",
"NPC: Io sono il guardiano di questa città.",
"Eroe: Ho un dono per la principessa da parte del mio maestro.",
];
start_dialog(&dialoghi); // Passiamo un riferimento al vettore
}
fn start_dialog(dialoghi: &[&str]) {
let mut indice: usize = 0; // Indice per navigare nel dialogo
loop {
let input = input_dialog();
if input.trim() == "q" {
println!("Hai terminato la conversazione.");
break;
} else if input.trim() == "a" {
if indice < dialoghi.len() {
println!("{}", dialoghi[indice]);
indice += 1;
} else {
println!("Fine del dialogo.");
break;
}
} else if input.trim() == "b" {
if indice > 0 {
indice -= 1;
println!("{}", dialoghi[indice]);
} else {
println!("Sei già all'inizio del dialogo.");
}
}
}
}
fn input_dialog() -> String {
println!("Premi 'a' per avanzare, 'b' per tornare indietro, 'q' per uscire.");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input
}