作为对for_each() 比map() 更可取的答案的补充(因为我们不消耗map() 发出的内容),下面是一个更简单的示例,试图说明问题(以及为什么借用-checker 在禁止此类尝试时是正确的)。
在这两种情况下(test1() 和 test2()),我们在扩展向量的同时迭代它(这是问题中的意图)。
在test1() 中,迭代器在创建时会一次性考虑值的存储。
对于所有后续迭代,它将引用此初始存储,因此此存储同时不得移动到内存中的其他位置。
这就是迭代器借用向量的原因(可变与否,这在这里并不重要)。
然而,在这些迭代过程中,我们尝试将新值附加到该向量:这可能会移动存储空间(出于重新分配目的)并且幸运的是这需要向量的可变借用(然后它被拒绝)。
在test2() 中,我们避免保留对初始存储的引用,而是使用计数器。
这行得通,但这是次优的,因为在每次迭代中,此索引操作 ([]) 都需要检查边界。
前一个函数中的迭代器知道所有的边界;这就是为什么迭代器会为编译器带来更好的优化机会。
请注意,len() 在循环开始时被评估一次;这可能是我们想要的,但如果我们想在每次迭代中重新评估它,那么我们将不得不使用loop {} 指令。
这里讨论的不是特定于语言,而是特定于问题本身。
使用更宽松的编程语言,第一次尝试可能是允许的,但会导致内存错误;或者这样的语言应该系统地转向第二次尝试,并在每次迭代时支付边界检查的成本。
最后,带有第二个循环的解决方案可能是最好的选择。
fn test1() {
let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
v.iter_mut().for_each(|e| {
if *e <= 3 {
let n = *e + 100;
// v.push(n) // !!! INCORRECT !!!
// we are trying to reallocate the storage while iterating over it
} else {
*e += 10;
}
});
println!("{:?}", v);
}
fn test2() {
let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
for i in 0..v.len() {
let e = &mut v[i];
if *e <= 3 {
let n = *e + 100;
v.push(n);
} else {
*e += 10;
}
}
println!("{:?}", v);
}
fn main() {
test1(); // [1, 2, 3, 14, 15, 16, 17, 18]
test2(); // [1, 2, 3, 14, 15, 16, 17, 18, 101, 102, 103]
}