如何在 Rust 中修复“在循环的前一个迭代中,..被可变地借用了”?

19

我需要遍历键,通过键在HashMap中查找值,在找到的作为值的结构体中进行一些重计算(懒惰=>变异结构体),并在Rust中缓存返回它。

我收到以下错误消息:

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:25:26
   |
23 |     fn it(&mut self) -> Option<&Box<Calculation>> {
   |           - let's call the lifetime of this reference `'1`
24 |         for key in vec!["1","2","3"] {
25 |             let result = self.find(&key.to_owned());
   |                          ^^^^ `*self` was mutably borrowed here in the previous iteration of the loop
...
28 |                 return result
   |                        ------ returning this value requires that `*self` is borrowed for `'1`

这里是代码演示

use std::collections::HashMap;

struct Calculation {
    value: Option<i32>
}

struct Struct {
    items: HashMap<String, Box<Calculation>> // cache
}

impl Struct {
    fn find(&mut self, key: &String) -> Option<&Box<Calculation>> {
        None // find, create, and/or calculate items
    }

    fn it(&mut self) -> Option<&Box<Calculation>> {
        for key in vec!["1","2","3"] {
            let result = self.find(&key.to_owned());
            if result.is_some() {
                return result
            }
        }
        None
    }
}
  • 由于我必须检查多个键值,所以无法避免循环
  • 由于可能的计算会更改它,我必须使其可变(self 和结构)

有什么建议可以改变设计(因为 Rust 强制以有些不同的方式思考),或者解决它的方法吗?

附言:代码中还有其他问题,但让我们先分割问题并解决这个。


同样相关的是:为什么不建议将对String(&String)、Vec(&Vec)或Box(&Box)的引用作为函数参数接受? - trent
2
帮助但仍然不明白为什么它不能编译。好的,self 被借用直到函数调用 self.find(...) 结束,甚至在下一次迭代之前 result 的生命周期也结束了。所以,“为什么”和“在循环的上一个迭代中被可变地借用”是一个错误,如果它不是错误,我们如何错误地使用它。即使简要解释也会有所帮助,这个错误让我感到困惑:' ) - AdityaG15
1
这是当前借用检查器的限制。Polonius接受它。 - Chayim Friedman
1
Polonius是借用检查器的下一个版本,https://github.com/rust-lang/polonius。 - Benjamin Smus
3个回答

6
排它锁不能用于缓存。在Rust中,您不能像使用通用指针那样使用引用(顺便说一下:&String&Box<T>是双重间接的,在Rust中不常见。请使用&str&T进行临时借用)。 &mut self 不仅表示可变性,还表示独占性和可变性,因此您的缓存只支持返回一个项目,因为它返回的引用必须在其存在期间保持self“锁定”。
您需要确信 ,find返回的内容不会在下一次调用它时突然消失。当前没有这样的保证,因为该接口不会阻止您调用例如items.clear()(借用检查器检查函数接口允许什么,而不是函数实际上执行了什么)。
您可以通过使用Rc或使用实现内存池/区域的crate来实现。
struct Struct {
   items: HashMap<String, Rc<Calculation>>,
}

fn find(&mut self, key: &str) -> Rc<Calculation> 

如果您克隆Rc,它将独立于缓存而存在,直到不再需要为止。

您还可以使用内部可变性使其更加优雅。

struct Struct {
   items: RefCell<HashMap<…
}

这将使您的备忘录化find方法使用共享借用而不是独占借用:
fn find(&self, key: &str) ->

这使得调用该方法的用户更容易使用。


为了修改Calculation(Rc :: get_mut()),必须使Rc <Calculation> strong_count = 0,但它可以在其他地方使用。基本上,这会锁定突变直到它被读取或在某个地方保持。因此,如果我理解正确的话,仅使用“RC”是不起作用的(与编译问题不同,我们有相同的运行时问题)。 - 4ntoine
如果您需要共享可变性(这会导致缓存条目也被改变),那么请使用 Rc<RefCell<Calculation>> - Kornel

5

这可能不是最干净的方法,但它可以编译。思路是不将找到的值存储在临时结果中,以避免别名问题:如果存储结果,self 将保持借用。

impl Struct {

    fn find(&mut self, key: &String) -> Option<&Box<Calculation>> {
        None
    }

    fn it(&mut self) -> Option<&Box<Calculation>> {
        for key in vec!["1","2","3"] {
            if self.find(&key.to_owned()).is_some() {
                return self.find(&key.to_owned());
            }
        }
        None
    }
}

似乎这不利于性能:find被调用了两次。应该有一个“正确的方法”。 - 4ntoine
2
我同意你的观点,这个做法可能有待改进。但是就你的具体情况而言,find 并不算太消耗资源,因为它只是在哈希表中进行查找,并且你知道该值已经存在。如果我理解你的意图正确,那么重要的计算无论如何都只需要执行一次,所以相比于那个操作,这个双重调用可能非常小。我会尝试想出更好的解决方案。 - Bromind

0

我曾经遇到类似的问题,后来通过将 for 循环转换为 fold 来解决了这个问题,这样编译器就会相信 self 没有被多次可变借用。

这种方法不需要使用内部可变性或重复函数调用,唯一的缺点是如果结果早已找到,它将不会短路而是继续迭代直到结束。

之前的代码:

for key in vec!["1","2","3"] {
    let result = self.find(&key.to_owned());
      if result.is_some() {
          return result
      }
}

之后:

vec!["1", "2,", "3"]
    .iter()
    .fold(None, |result, key| match result {
        Some(result) => Some(result),
        None => self.find(&key.to_string())
    })

工作区链接: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=92bc73e4bac556ce163e0790c7d3f154


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