ProgrammazioneSviluppatore Rust / Sviluppatore Backend

Racconta come funzionano gli iteratori e gli adattatori di iteratori in Rust. Qual è la differenza tra l'implementazione di un iteratore personalizzato e l'uso di adattatori standard, e in quali casi è opportuno implementare un proprio iteratore?

Supera i colloqui con l'assistente IA Hintsage

Risposta

In Rust, l'intera libreria di collezioni standard si basa sul concetto di iteratori. Un iteratore è un oggetto che implementa il trait Iterator, in cui è definito il metodo next(). Questo metodo restituisce il prossimo elemento di una sequenza di dati (Option<T>, dove Some(T) è il valore successivo, None indica la fine della sequenza).

Esempio di iteratore standard:

let v = vec![1, 2, 3]; let mut iter = v.iter(); while let Some(x) = iter.next() { println!("{}", x); }

Adattatori di iteratori sono metodi (ad esempio, .map(), .filter(), .enumerate(), .take()) che restituiscono nuovi iteratori, elaborando i valori "al volo" secondo le funzioni fornite.

Iteratori personalizzati vengono implementati creando una struttura e implementando per essa il trait Iterator con un comportamento personalizzato:

struct Counter { count: u8 } impl Iterator for Counter { type Item = u8; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } }

Utilizzare adattatori standard quando sono sufficienti per elaborare collezioni. L'implementazione di un proprio iteratore è necessaria se:

  • È necessario rappresentare una sequenza generata algoritmicamente / pigra.
  • È necessario un controllo approfondito sul processo.
  • È necessario integrarsi con fonti di dati esterne/non standard.

Domanda trabocchetto

È possibile creare iteratori infiniti? Cosa succede se si prova a raccoglierli in una collezione, ad esempio, utilizzando .collect()?

Risposta: Sì, in Rust ci sono iteratori come std::iter::repeat, che restituiscono una sequenza infinita:

let mut endless = std::iter::repeat(1); endless.next(); // restituirà Some(1) all'infinito

Se si prova a raccogliere un tale iteratore in una collezione tramite .collect(), il programma si bloccherà o uscirà con un overflow di memoria, poiché l'iterazione non si completerà da sola!

Esempi di errori reali dovuti alla mancanza di conoscenze sulle sottigliezze dell'argomento.


Storia

In un progetto REST API, un array di 1000 elementi è stato ordinato e poi è stato utilizzato .iter().filter(|x| *x > 500), ma invece di .collect::<Vec<_>>() è stato applicato .fold(0, |acc, _| acc + 1) all'interno di un adattatore complesso, perdendo la comprensione di se l'iterazione si sarebbe conclusa correttamente. Risultato: blocchi casuali a causa del fatto che il filtraggio avveniva attraverso la pigrizia illimitata dell'iteratore con un errore interno.


Storia

In un motore proprietario per la generazione di ID unici, un sviluppatore a caso ha deciso di implementare il proprio iteratore, dimenticando di restituire None al raggiungimento del limite. Di conseguenza, l'iterazione entrava in un ciclo infinito, il server in produzione consumava tutta la CPU e non rispondeva alle richieste.


Storia

Nella parte frontend (modulo WebAssembly) è stato utilizzato un iteratore con adattatori annidati: .map().filter().skip(), e si è cercato di ottenere il risultato tramite .collect() per un modulo. Modificando il tipo sotto l'adattatore, Rust ha generato un errore complesso di tempo di compilazione riguardo alla non corrispondenza dei tipi, perché era stata dimenticata l'indicazione del tipo esatto della collezione: .collect::<Vec<_>>(). Il problema è stato risolto aggiungendo un'annotazione, ma sono state spese diverse ore per cercare la causa.