在迭代另一个属性时修改结构体的一个属性

3

我有一个包含2个Vec的结构体。我想要在修改其中一个的同时遍历另一个。下面是一个示例程序:

use std::slice;

struct S {
    a: Vec<i32>,
    b: Vec<i32>
}

impl S {
    fn a_iter<'a>(&'a self) -> slice::Iter<i32>  {
        self.a.iter()
    }
    fn a_push(&mut self, val: i32) {
        self.a.push(val);
    }
    fn b_push(&mut self, val: i32) {
        self.b.push(val);
    }
}

fn main() {
    let mut s = S { a: Vec::new(), b: Vec::new() };
    s.a_push(1);
    s.a_push(2);
    s.a_push(3);

    for a_val in s.a_iter() {
        s.b_push(a_val*2);
    }
}

但是有一个编译错误:
$ rustc iterexample.rs 
iterexample.rs:28:9: 28:10 error: cannot borrow `s` as mutable because it is also borrowed as immutable
iterexample.rs:28         s.b_push(a_val*2);
                           ^
note: in expansion of for loop expansion
 iterexample.rs:26:5: 29:6 note: expansion site
iterexample.rs:26:18: 26:19 note: previous borrow of `s` occurs here; the immutable borrow prevents subsequent moves or mutable borrows of `s` until the borrow ends
iterexample.rs:26     for a_val in s.a_iter() {
                                   ^
note: in expansion of for loop expansion
iterexample.rs:26:5: 29:6 note: expansion site
iterexample.rs:29:6: 29:6 note: previous borrow ends here
iterexample.rs:26     for a_val in s.a_iter() {
iterexample.rs:27         println!("Looking at {}", a_val);
iterexample.rs:28         s.b_push(a_val*2);
iterexample.rs:29     }
                      ^
note: in expansion of for loop expansion
iterexample.rs:26:5: 29:6 note: expansion site
error: aborting due to previous error

我了解编译器的报错原因。在for循环中,我使用了self,因为我仍然在遍历它。

但从理论上讲,应该有一种方法可以实现这一点。我只修改了s.b,并未修改正在遍历的东西(s.a)。是否有办法编写程序以演示这种分离,并允许此类程序编译通过?

这是一个更大程序的简化示例,因此我需要保持相同的总体结构(一个具有某些内容的结构体,其中一个将被迭代,另一个将被更新)。


我可能非常错误,不要听取我的话,这只是一个猜测,但我认为你不能这样做的概念原因是,如果将s.b推送到内存中导致s在内存中被重新定位,那么对s.a的引用将无效。不过,你可以使用s.a.clone().iter()代替s.a.iter()来解决这个问题。 - Adrian
1
@Adrian 使用s.a.iters.b.push实际上是可以正常工作的。我怀疑@Rory想要在他们的非简化代码中保持字段私有(或者a_iterb_push可能实际上更复杂,不仅仅是Vec方法的简单包装)。 - fjh
@Adrian,你的推理不成立的地方在于,将元素推入s.b不会重新定位s,但会重新定位由s.b拥有并由其私有ptr字段指向的内部内存块(请参见Vec的实现)。 - mdup
@fjh 你说得对。我想保持这些事情的私密性。我希望这些函数可以作为更复杂事物的“抽象”。 - Amandasaurus
@Adrian 尝试过了,但是出现了关于 a_iter 的新错误,提示“借用值的生命周期不够长”。之前的错误“无法将 s 作为可变借用,因为它也被作为不可变借用”仍然存在。 - Amandasaurus
4个回答

4
如果你使用s.a.it而不是s.a_iter(),就可以消除错误。 你当前的解决方案无法工作,因为从s.a_iter()返回的迭代器保留了对s的引用,该引用与s本身具有相同的生命周期,因此在该引用存在期间,您无法作为可变量借用s内的任何内容。具体而言,这是因为:

当评估通用参数(在您的情况下为生存期)时,编译器会停止在函数调用边界处。

这里有一个很好的答案,其中包含非常类似的问题的完整解释: cannot borrow `self.x` as immutable because `*self` is also borrowed as mutable

编辑

一种可能的解决方法是将操作带到S内部,而不是将迭代器从S带出来。您可以在S中定义此类方法:

fn foreach_in_a_push_to_b<F>(&mut self, func: F) where F : Fn(&i32) -> i32 {
    for a_val in self.a.iter() {
        self.b.push(func(a_val));
    }
}

然后

s.foreach_in_a_push_to_b(|&x| x * 2);

是的,那可能会解决这个例子。但是我想保持这个“API”不变,也就是说,我需要一个可以用来迭代的函数。我不想“伸手进去”结构体里面。 - Amandasaurus
@Rory,将迭代和推送逻辑作为S上的一个方法公开是否有意义?在其中,您可以直接访问ab而无需使它们公开。显然,如果不知道具体情况,我无法判断这是否有意义。 - fjh
@fjh 目前这是我能想到的唯一解决方案,但也许对于 Rory 来说还不够。我已经更新了答案并附上了一个例子。 - eulerdisk
可能可以行。问题是我想要从 Vec 中交换 ab(在实际版本中它们是 HashMaps),使用更复杂的数据结构,这些数据结构将具有更复杂的 pushiter 逻辑... - Amandasaurus

2

根本问题在于借用检查器没有足够的信息来证明您的代码是安全的;它只停留在函数边界。您可以编写一个方法来拆分引用,以便编译器有所需要的信息:does

struct S {
    a: Vec<i32>,
    b: Vec<i32>
}

impl S {
    fn a_push(&mut self, val: i32) {
        self.a.push(val);
    }
    fn split_a_mut_b<'a>(&'a mut self) -> (&'a Vec<i32>, &'a mut Vec<i32>) {
        (&self.a, &mut self.b)
    }
}

fn main() {
    let mut s = S { a: Vec::new(), b: Vec::new() };
    s.a_push(1);
    s.a_push(2);
    s.a_push(3);

    let (a, b) = s.split_a_mut_b();

    for a_val in a.iter() {
        b.push(a_val*2);
    }
}

关键在于在split_a_mut_b函数内部,编译器可以证明这两个借用没有重叠。另一个模式是将值暂时拆分为可变和不可变部分,可以保留更多原始API:

use std::slice;

#[derive(Debug)]
struct S {
    a: Vec<i32>,
    b: Vec<i32>
}

impl S {
    fn a_iter(&self) -> slice::Iter<i32>  {
        self.a.iter()
    }
    fn a_push(&mut self, val: i32) {
        self.a.push(val);
    }
    fn b_push(&mut self, val: i32) {
        self.b.push(val);
    }
    fn split_a_mut_b<F, R>(&mut self, f: F) -> R
    where F: FnOnce(&Self, &mut Self) -> R {
        use std::mem::swap;

        // Break off the mutable part(s) (or the immutable parts if there
        // are less of those).
        let mut temp = S { a: vec![], b: vec![] };
        swap(&mut self.b, &mut temp.b);

        // Call the closure.
        let result = f(self, &mut temp);

        // Glue the value back together.
        swap(&mut self.b, &mut temp.b);

        result
    }
}

fn main() {
    let mut s = S { a: Vec::new(), b: Vec::new() };
    s.a_push(1);
    s.a_push(2);
    s.a_push(3);

    s.split_a_mut_b(|imm, muta| {
        for a_val in imm.a_iter() {
            muta.b_push(a_val*2);
        }
    });

    println!("{:?}", s);
}

这并不是非常低效的方法;这种方法完全没有堆活动;我们只是在调整指针。

0
使用原始指针,您可以将结构别名为第二个变量--Rust 将把它们视为两个不同的变量,并允许您借用不可变部分而不会发出警告。
let s_alias = &s as *const S;
let a_iter = unsafe { (*s_alias).a_iter() };

for a_val in a_iter {
    s.b_push(a_val*2);
}

游乐场

我欢迎对此的第二意见,但至少在这个例子中,我不认为它会导致任何内存安全问题。


0

我认为我已经使用宏“解决”了这个问题。如果我使用以下代码,它就可以工作:

use std::slice;

struct S {
    a: Vec<i32>,
    b: Vec<i32>
}

impl S {
    fn a_push(&mut self, val: i32) {
        self.a.push(val);
    }
}

macro_rules! a_iter {
    ($x: expr) => {
        { $x.a.iter() }
    }
}

macro_rules! b_push {
    ($x: expr, $val: expr) => {
        $x.b.push($val);
    }
}

fn main() {
    let mut s = S { a: Vec::new(), b: Vec::new() };
    s.a_push(1);
    s.a_push(2);
    s.a_push(3);

    let iter = a_iter!(s);

    for a_val in iter {
        println!("Looking at {}", a_val);
        b_push!(s, a_val*2);
    }
}

在这里,我将a_iterb_push代码移入了一个宏中。当代码被编译时,宏将被展开,就好像我们没有使用抽象函数。但是对于编写代码来说,该功能被抽象化了。
我不确定这是个好主意还是坏主意。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接