为什么这个可变借用超出了其作用域的生命周期?

15
我遇到了一个混乱的错误,关于同时使用可变和不可变借用,在我期望可变借用结束后。我已经对类似问题进行了大量研究(1, 2, 3, 4, 5),这使我相信我的问题与词法生命周期有关(尽管打开NLL功能并在夜间编译时不会改变结果),但我不知道具体是什么;我的情况似乎不符合其他问题的任何情况。
pub enum Chain<'a> {
    Root {
        value: String,
    },
    Child {
        parent: &'a mut Chain<'a>,
    },
}

impl Chain<'_> {
    pub fn get(&self) -> &String {
        match self {
            Chain::Root { ref value } => value,
            Chain::Child { ref parent } => parent.get(),
        }
    }

    pub fn get_mut(&mut self) -> &mut String {
        match self {
            Chain::Root { ref mut value } => value,
            Chain::Child { ref mut parent } => parent.get_mut(),
        }
    }
}

#[test]
fn test() {
    let mut root = Chain::Root { value: "foo".to_string() };

    {
        let mut child = Chain::Child { parent: &mut root };

        *child.get_mut() = "bar".to_string();
    } // I expect child's borrow to go out of scope here

    assert_eq!("bar".to_string(), *root.get());
}

playground

出现的错误是:

error[E0502]: cannot borrow `root` as immutable because it is also borrowed as mutable
  --> example.rs:36:36
   |
31 |         let mut child = Chain::Child { parent: &mut root };
   |                                                --------- mutable borrow occurs here
...
36 |     assert_eq!("bar".to_string(), *root.get());
   |                                    ^^^^
   |                                    |
   |                                    immutable borrow occurs here
   |                                    mutable borrow later used here

我理解为什么会有一个不可变借用,但我不明白如何在那里使用可变借用。两者如何同时使用?希望有人能解释发生了什么以及如何避免。

2个回答

17
简而言之,&'a mut Chain<'a>非常限制且普遍。
对于不可变引用&T<'a>,编译器允许在必要时缩短'a的生命周期以匹配其他生命周期或作为NLL的一部分(这并非总是如此,这取决于T是什么)。但是,它不能对可变引用&mut T<'a>这样做,否则您可以将其赋值为具有更短生命周期的值。
因此,当编译器在链接引用和参数时协调生命周期时,引用的生命周期在概念上扩展为与参数的生命周期相匹配。这基本上意味着您创建了一个永远不会释放的可变借用。
将该知识应用于您的问题:只有嵌套值在其生命周期上是协变的情况下,才真正可能创建基于引用的层次结构。这排除了:
  • 可变引用
  • 特质对象
  • 带内部可变性的结构体
请参阅playground上的这些变化,以查看它们的预期效果。
另请参见:
  • 为什么仅涉及可变引用时链接生命周期很重要?
  • 如何使用特质对象链实现责任链模式?
  • 在具有自链接生命周期的可变引用中获取不可变引用时有何区别?

  • 为了有趣,我会包括一个例子,在这个例子中 Rust 标准库故意做了这样的事情。std::thread::scope 的签名看起来像:

    pub fn scope<'env, F, T>(f: F) -> T
    where
        F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T
    

    提供给用户定义函数的Scope故意将其生命周期绑在一起,以确保它仅以预期的方式使用。这并非总是如此,因为结构体可能在其泛型类型上是协变或逆变的,但是Scope被定义为不变的。另外,唯一可在其上调用的函数是.spawn(),该函数故意将&'scope self作为self参数,从而确保引用的生命周期不短于由scope给出的生命周期。

    在内部,标准库包含此文档(source):

    Invariance over 'scope, to make sure 'scope cannot shrink, which is necessary for soundness.

    Without invariance, this would compile fine but be unsound:

    std::thread::scope(|s| {
        s.spawn(|| {
            let a = String::from("abcd");
            s.spawn(|| println!("{a:?}")); // might run after `a` is dropped
        });
    });
    
    即使参考的生命周期相对于自身是不变的,这仍然避免了许多问题,因为它使用了一个不可变的引用和内部可变性。如果.spawn()的参数需要&'scope mut self,那么当尝试生成多个线程时,它将无法工作并遇到上述相同的问题。

谢谢。我现在明白为什么它不起作用了。我认为我需要的解决方案可能在你提供的链接中的某一个里面,尤其是责任链模式——那几乎就是我试图实现的模式。 - rmeador

4
问题不在于词法生命周期,添加显式的 drop 也无法改变错误。问题出在 &'a mut Chain<'a> 上,这强制 root 借用其整个生命周期,在借用被解除后,root 就无法再使用了。如下评论所述,使用生命周期来解决这个问题基本上是不可能的。我建议使用 Box 代替。将结构体更改为:
pub enum Chain{
Root {
        value: String,
    },
    Child {
        parent: Box<Chain>,
    },
}

并根据需要调整其他方法。或者,如果您希望原始副本保持可用而不消耗自身,则可以使用Rc<RefCell<Chain>>


@kmdreko 你是对的,我那么做真的不太聪明。已经修复了。 - Aiden4
谢谢。我可能能够重构我的设计来使用Box,但是我经常在链的末尾推送和弹出这些Child对象,如果它不是由Root拥有,那么思考起来会更加困难。 - rmeador
2
@rmeador 或许值得尝试使用 Rc<RefCell<Chain>>Rc 允许您共享所有权,而 RefCell 则允许您动态地强制执行 RAII 而不是静态地执行。但如果这样做,请小心引用循环。 - Aiden4

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