rustiterator

jump out of Vec::retain()?


I want to iterate through a Vec doing various things to the objects in it (which would be more complex than usizes), and, in the event of an order to stop iterating, retain only the objects which haven't yet been processed.

fn main() {
    let _ = iterate();
}

fn iterate(){
    let mut objs_for_processing = Vec::new();
    
    for i in 0..10 {
        objs_for_processing.push(i);
    }
    
    let mut n = 0;
    objs_for_processing.retain(|obj| {
        n += 1;
        if n > 3 {
            println!("STOP iteration!...");
            return true // i.e. retain
        }
        println!("iteration, do some business with obj {}", obj);
        false
    });
    println!("objs_for_processing.len() {} {:#?}", objs_for_processing.len(), objs_for_processing);
}

The problem with the above, fairly obviously, is that you have to complete all the iterations, even after you know you want to retain all the other elements...

I found this question and this question. The latter seemed more promising. But then it turns out that you can't change the return type of retain's closure (i.e. to accept a Result, so you can then return an Err which will stop the iteration), as far as I can tell.

The only solution I've found is to clone the Vec, iterate using a normal for ... in and then remove items from the clone (called depleted_vec) as they are processed, using the rather cumbersome

let index = depleted_vec.iter().position(|x| x == obj).unwrap();
depleted_vec.remove(index);

... and then to assign objs_for_processing to depleted_vec. At least this way I can use break to stop the iterating.

It seems like a fairly reasonable thing to want to do: isn't there some more elegant way to do this?

NB I also wondered whether there might be an Iterator-based solution so I tried with iter().filter(...).collect() ... this produces a Vec of &usize, not usize. So it could be used maybe but at cost of additional processing. Not pretty: it just feels like there should be something better.


Solution

  • So if I understand this correctly, you need to do two things:

    • Iterate over the elements of the vec, doing things with the objects there (modifying them?)
    • At some point during iteration, something forces you to stop (e.g. a buffer is full). At this point, you want to remove all the processed items from the vec, so that you can resume work later presumably.

    Here's the loop to accomplish this efficiently:

    let mut n = 0;
    for e in &objs_for_processing {
      if must_stop() { break; }
      n += 1;
      process(e);
    }
    objs_for_processing.drain(0..n);
    

    I don't see a point in coming up with anything else, though the experimental extract_if API would serve your use case well (would still iterate over everything though).

    However, if this is really a work queue, you're much better off using a VecDeque in the first place.

    use std::collections::VecDeque;
    
    fn main() {
        let mut objs_for_processing = (0..10).collect::<VecDeque<_>>();
    
        let mut n = 0;
        while let Some(obj) = objs_for_processing.pop_front() {
            n += 1;
            if n > 3 {
                println!("STOP iteration!...");
                break;
            }
            println!("iteration, do some business with obj {}", obj);
        }
        println!(
            "objs_for_processing.len() {} {:#?}",
            objs_for_processing.len(),
            objs_for_processing
        );
    }
    

    Link to playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=78468b26d34a29009b090a969fd9d5f3