在一个代码中,无法同时多次借用为可变引用 - 但在另一个非常相似的代码中可以。

11

我有一个代码片段,它无法通过借用检查器:

use std::collections::HashMap;

enum Error {
    FunctionNotFound,
}

#[derive(Copy, Clone)]
struct Function<'a> {
    name: &'a str,
    code: &'a [u32],
}

struct Context<'a> {
    program: HashMap<&'a str, Function<'a>>,
    call_stack: Vec<Function<'a>>,
}

impl<'a> Context<'a> {
    fn get_function(&'a mut self, fun_name: &'a str) -> Result<Function<'a>, Error> {
        self.program
            .get(fun_name)
            .map(|f| *f)
            .ok_or(Error::FunctionNotFound)
    }

    fn call(&'a mut self, fun_name: &'a str) -> Result<(), Error> {
        let fun = try!(self.get_function(fun_name));

        self.call_stack.push(fun);

        Ok(())
    }
}

fn main() {}
error[E0499]: cannot borrow `self.call_stack` as mutable more than once at a time
  --> src/main.rs:29:9
   |
27 |         let fun = try!(self.get_function(fun_name));
   |                        ---- first mutable borrow occurs here
28 | 
29 |         self.call_stack.push(fun);
   |         ^^^^^^^^^^^^^^^ second mutable borrow occurs here
...
32 |     }
   |     - first borrow ends here

我的直觉是问题与HashMap返回数据结构内部的None或值的引用有关。但我不希望这样:我的意图是self.get_function应该返回存储值的字节副本或错误(这就是我加上 .map(|f| *f)FunctionCopy 的原因)。

&'a mut self更改为其他内容并没有帮助。

然而,下面的代码片段在精神上有些相似,但被接受:

#[derive(Debug)]
enum Error {
    StackUnderflow,
}

struct Context {
    stack: Vec<u32>,
}

impl Context {
    fn pop(&mut self) -> Result<u32, Error> {
        self.stack.pop().ok_or(Error::StackUnderflow)
    }

    fn add(&mut self) -> Result<(), Error> {
        let a = try!(self.pop());
        let b = try!(self.pop());

        self.stack.push(a + b);
        Ok(())
    }
}

fn main() {
    let mut a = Context { stack: vec![1, 2] };
    a.add().unwrap();
    println!("{:?}", a.stack);
}

现在我感到困惑。第一个代码片段有什么问题?为什么第二个没有这个问题?

这些片段是更大的一段代码的一部分。为了提供更多的上下文,这里在 Rust Playground 上展示了一个包含有错误代码的更完整的示例而这里则展示了一个更早版本的示例,其中没有使用 HashMap


2
我认为fun_name不应该有任何生命周期。 - o11c
你的第二个代码片段根本没有生命周期,因此它并不符合“相似”的条件。 - oli_obk
2个回答

9
你陷入了一种终身陷阱。将相同的lifetime添加到更多的引用中会更加限制你的程序。添加更多的lifetimes并给每个引用分配最小可能的lifetime将允许更多的程序。正如@o11c所指出的,移除对'a lifetime'的约束将解决你的问题。
impl<'a> Context<'a> {
    fn get_function(&mut self, fun_name: &str) -> Result<Function<'a>, Error> {
        self.program
            .get(fun_name)
            .map(|f| *f)
            .ok_or(Error::FunctionNotFound)
    }

    fn call(&mut self, fun_name: &str) -> Result<(), Error> {
        let fun = try!(self.get_function(fun_name));

        self.call_stack.push(fun);

        Ok(())
    }
}

这个技术的原理是Rust会插入新的生命周期,因此在编译器中,函数的签名看起来像这样:
fn get_function<'b>(&'b mut self, fun_name: &'b str) -> Result<Function<'a>, Error>
fn call<'b>(&'b mut self, fun_name: &'b str) -> Result<(), Error>

尽量避免使用生命周期,让编译器更智能。如果不能实现,不要在所有地方都添加生命周期,考虑在哪里传递所有权以及在何处限制引用的生命周期。


谢谢!我尝试放置fn get_function<'b>(&'b mut self, fun_name: &'b str) -> Result<Function<'b>, Error>,但错误变得更奇怪了。似乎在这个方法中Function不能有与fun_name相同的生命周期,但我不太理解为什么。 - darque
1
我不知道为什么你想要限制生命周期。通常情况下,你会尽可能地释放生命周期,编译器已经对你进行了足够的限制。 - oli_obk
1
我猜你的 name 字段的生命周期可能应该是 'static 而不是 'a。如果不行的话,很有可能你想要那个字段是一个 String 类型。对于 HashMap 的键类型也是一样的情况。 - oli_obk
1
我本来想把所有这样的字符串都分配到一个TypedArena中,并且只使用&str引用它们,但是我认为你是对的。然而,它们不能是'static,因为我希望使用编译器生成代码。 - darque
添加更多的生命周期并给每个引用最小可能的生命周期将允许更多的程序——那个逻辑似乎是反过来的;你是不是想说更少的程序? - Shepmaster
也许我应该完全删除那个语句。我的意思是您可以从更多的上下文中调用该函数。 - oli_obk

8

你只需要删除不必要的生命周期限定符,就能使你的代码编译通过:

fn get_function(&mut self, fun_name: &str) -> Result<Function<'a>, Error> { ... }

fn call(&mut self, fun_name: &str) -> Result<(), Error> { ... }

你的问题在于将 &mut self 的生命周期和存储在其中的值 (Function<'a>) 的生命周期绑定在一起,而这在大多数情况下是不必要的。由于此依赖关系存在于 get_function() 的定义中,编译器必须假设调用 self.get_function(...) 的结果借用了 self,因此会禁止你再次借用它。 &str 参数上的生命周期也是不必要的 - 它只是无缘无故地限制了参数值的可能集。你的键可以是具有任意生命周期的字符串,而不仅仅是 'a

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