【问题标题】:Modifying one attribute of a struct while iterating over another attribute在迭代另一个属性时修改结构的一个属性
【发布时间】:2015-09-20 12:06:29
【问题描述】:

我有一个有 2 个Vecs 的结构。我希望能够在修改另一个的同时迭代一个。这是一个示例程序:

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 使用 s.a.iter 和 s.b.push 实际上可以正常工作。我怀疑@Rory 希望在其非简化代码中保持字段私有(或者a_iter 和b_push 实际上可能更复杂,而不仅仅是Vec 方法的简单包装)。
  • @Adrian 您的推理不成立的地方是,推送到s.b 不会重定位s,但会重定位s.b 拥有的内部内存块,并由其特别指出私有ptr 字段(参见Vec 的实现)。
  • @fjh 你是对的。我想保密。我希望这些函数是对更复杂事物的“抽象”。
  • @Adrian 试过了,然后在a_iter 中得到一个关于“借用值不够长”的新错误。也得到与以前相同的错误“不能借用s作为可变的,因为它也被借用为不可变的”

标签: iterator rust borrow-checker


【解决方案1】:

如果您使用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);

【讨论】:

  • 是的,这可能会解决 this 示例。但是我想保持这个“API”不变,也就是说,我需要一个可以用来迭代的函数。我不想“进入”结构。
  • @Rory 将迭代和推送逻辑公开为S 上的方法是否有意义?在那里,您可以直接访问 a 和 b 而无需公开它们。如果不知道具体细节,我显然无法判断这是否有意义。
  • @fjh 目前这是我能想到的唯一解决方案,但对 Rory 来说可能还不够。我用一个例子更新了答案。
  • (编辑后) 可以。问题是我想用更复杂的数据结构将a 和b 从Vec 换出(在真实版本中它们是HashMaps),对于push 和@987654341 会有更复杂的逻辑@...
【解决方案2】:

根本问题是借用检查器没有足够的信息来证明您的代码是安全的;它停在功能边界处。您可以通过编写一个拆分引用的方法来解决此问题,以便编译器确实拥有它需要的信息:

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

这不是非常低效;此方法引入绝对没有堆活动;我们只是在改变指针。

【讨论】:

    【解决方案3】:

    使用原始指针,您可以将结构别名为第二个变量——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);
    }
    

    Playground

    我欢迎对此提出第二意见,但我看不出它会如何导致任何内存安全问题,至少在这个例子中是这样。

    【讨论】:

      【解决方案4】:

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

      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_iter 和 b_push 代码移动到一个宏中。编译代码时,宏将被扩展,就好像我们没有使用抽象函数一样。但是对于编写代码,该功能被抽象掉了。

      我不确定它们是好主意还是坏主意。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-02-21
        • 2017-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-09
        • 2020-09-01
        • 1970-01-01
        相关资源
        最近更新 更多