跳出 Vec::retain()?

jump out of Vec::retain()?

提问人:mike rodent 提问时间:11/14/2023 更新时间:11/14/2023 访问量:57

问:

我想遍历 a 对其中的对象执行各种操作(这将比 s 更复杂),并且在命令停止迭代的情况下,只保留尚未处理的对象。Vecusize

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);
}

很明显,上述的问题在于,即使你知道要保留所有其他元素,你也必须完成所有的迭代......

我找到了这个问题和这个问题。后者似乎更有前途。但后来事实证明,据我所知,您无法更改 's 闭包的返回类型(即接受 ,因此您可以返回 which 将停止迭代)。retainResultErr

我发现的唯一解决方案是克隆 ,使用正常迭代,然后在处理项目时从克隆中删除项目(称为 ),使用相当繁琐的Vecfor ... indepleted_vec

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

...,然后分配给 .至少这样我就可以用它来停止迭代。objs_for_processingdepleted_vecbreak

这似乎是一件相当合理的事情:难道没有更优雅的方法可以做到这一点吗?

注意:我也想知道是否可能有一个基于的解决方案,所以我尝试了......这会产生 的 ,而不是 。因此,也许可以使用它,但需要额外处理。不漂亮:只是感觉应该有更好的东西。Iteratoriter().filter(...).collect()Vec&usizeusize

Rust 迭代器

评论

0赞 Aleksander Krauze 11/14/2023
广告“基于迭代器的解决方案”。你已经得到了对向量内值的引用,因为将返回借用的迭代器。如果要使用原始向量,请改用。iter()into_iter
0赞 Aleksander Krauze 11/14/2023
还要记住,保留可能是昂贵的操作(因为您需要复制大量数据)。请记住,即使你想“停止”保留,如果删除了一些元素,你仍然需要复制向量的剩余尾部。也许您的用例更适合喜欢的列表?
0赞 mike rodent 11/14/2023
谢谢。 工作得很漂亮。首先,我收到关于“无法推断类型”的抱怨......但是,如果您将原始值设置为以下结果,则此问题将得到解决。诚然,这也涉及再次(在)中进行整个迭代......但似乎没有办法避免这种情况。into_iterVeccollectobjs_for_processing = objs_for_processing.into_iter().filter(|obj| { ...filter
0赞 Aleksander Krauze 11/14/2023
“这也涉及再次进行整个迭代” Rust 中的迭代器是懒惰的。您可以提前停止迭代和迭代器,这将释放剩余的矢量元素。drop
0赞 mike rodent 11/14/2023
好的,我纠正了。但我不明白你是如何从关闭中做到这一点的。或者,也许你的意思是以其他方式做?也许您可能想添加带有代码片段的答案?filter

答:

0赞 Chayim Friedman 11/14/2023 #1

你不需要克隆 ,你只需使用索引进行迭代并删除元素:Vec

let mut i = 0;
let mut n = 0;
while i < objs_for_processing.len() {
    if n > 3 {
        println!("STOP iteration!...");
        break;
    }
    
    if true {
        let obj = &objs_for_processing[i];
        println!("iteration, do some business with obj {}", obj);
        
        objs_for_processing.remove(i);
        // Don't increment `i`!
    } else {
        i += 1;
    }
    
    n += 1;
}

这不会像(因为它可以批量删除)那样有效,但这对您来说可能无关紧要。retain()

评论

0赞 mike rodent 11/14/2023
谢谢。我想我可以看到你在做什么,看起来这里有一个有效的解决方案......但正如所写的那样是无法访问的代码。i += 1;
0赞 Chayim Friedman 11/14/2023
@mikerodent是的。我添加它是为了你不想删除所有前三个元素的情况,只有当某个条件为真时。
2赞 Sebastian Redl 11/14/2023 #2

因此,如果我理解正确,您需要做两件事:

  • 遍历 vec 的元素,对那里的对象做事(修改它们?
  • 在迭代过程中的某个时刻,某些东西会迫使您停止(例如,缓冲区已满)。此时,您希望从 vec 中删除所有已处理的项目,以便稍后可以恢复工作。

以下是有效完成此操作的循环:

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

我认为想出其他任何东西都没有意义,尽管实验性 API 可以很好地服务于您的用例(尽管仍然会迭代所有内容)。extract_if

但是,如果这真的是一个工作队列,那么最好首先使用 VecDeque。

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
    );
}

游乐场链接: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=78468b26d34a29009b090a969fd9d5f3

评论

0赞 mike rodent 11/14/2023
伟大。是的,你已经准确地推测了我想做什么。我看到并想知道......我从未见过,也可能不需要它:乍一看,我看不出这会优于您出色而简单的解决方案(至少在单线程上下文中,这显然是)。Vec::drainVecDequefor
0赞 Sebastian Redl 11/14/2023
@mikerodent不需要在从前面移除东西时移动元素。这是一个很大的效率优势。VecDeque
0赞 mike rodent 11/14/2023
右。在我的用例中,这无关紧要,但会将其归档以备将来参考。我认为这是一种经过优化的丢弃切片的方法。drainVec
0赞 mike rodent 11/17/2023
事实上,作为一个政策问题,最好以相反的顺序构建它(在我的特定情况下,顺序是无关紧要的)......然后从 END 开始迭代 ...因为你的评论让我想起了我在 The Rust Book 中读到的关于分配 s 的东西。关键是(正如您所暗示的)在存储结束时释放内存比在开始时要容易得多。我不清楚(在我的水平上)作为 Rust 从业者,我们是否需要关注这个问题,或者 Rust 编译器/优化器是否太热了,以至于可以将这些问题抽象出来......VecVecVec
1赞 Sebastian Redl 11/17/2023
@mikerodent 不,这超出了优化器的能力范围。如果你不连续生产物品,订单也无关紧要,那么从后面工作确实更好,而且由于代码的可读性更强,你绝对应该这样做。