为什么这种可变借用超出了它的范围?

Why does this mutable borrow live beyond its scope?

提问人:rmeador 提问时间:2/18/2021 最后编辑:JamesThomasMoonrmeador 更新时间:12/27/2022 访问量:1349

问:

在我期望可变借用结束之后,我遇到了一个令人困惑的错误,即同时使用可变和不可变借用。我对类似的问题(12345)做了很多研究,这让我相信我的问题与词汇生命周期有关(尽管打开 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());
}

操场

错误是:

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

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

Rust 借用检查器

评论


答:

18赞 kmdreko 2/18/2021 #1

简而言之,这是极其有限和普遍的。&'a mut Chain<'a>

对于不可变引用,编译器可以在必要时缩短生存期以匹配其他生存期或作为NLL的一部分(情况并非总是如此,这取决于是什么)。但是,它不能对可变引用执行此操作,否则您可以为其分配一个生存期较短的值。&T<'a>'aT&mut T<'a>

因此,当编译器尝试协调引用和参数链接时的生存期时,引用的生存期在概念上会扩展以匹配参数的生存期。这实质上意味着你已经创建了一个永远不会释放的可变借用。&'a mut T<'a>

将这些知识应用于您的问题:只有当嵌套值在其生命周期内协变时,才有可能创建基于引用的层次结构。其中不包括:

  • 可变引用
  • trait 对象
  • 具有内部可变性的结构

请参阅 Playground 上的这些变体,看看它们如何无法按预期工作。

另请参阅:


为了好玩,我将包括一个 Rust 标准库故意做这种事情的案例。std::thread::scope 的签名如下所示:

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

有意将提供给用户定义函数的作用域的生存期捆绑在一起,以确保它仅以预期的方式使用。情况并非总是如此,因为结构体可能在其泛型类型上是协变的或逆变的,但被定义为不变的。此外,唯一可以调用的函数是有意将 self 也作为自参数的函数,以确保引用的生存期不会短于 给出的寿命。Scope.spawn()&'scope selfscope

在内部,标准库包含以下文档(来源):

不变性,以确保不会收缩,这是健全性的必要条件。'scope'scope

如果没有不变性,这将编译良好,但不合理:

std::thread::scope(|s| {
    s.spawn(|| {
        let a = String::from("abcd");
        s.spawn(|| println!("{a:?}")); // might run after `a` is dropped
    });
});

即使引用的生存期相对于自身是不变的,这仍然避免了上述许多问题,因为它使用不可变引用和内部可变性。如果参数为 required ,则这将不起作用,并且在尝试生成多个线程时遇到上述相同的问题。.spawn()&'scope mut self

评论

0赞 rmeador 2/19/2021
谢谢。我现在明白为什么它不起作用了。我认为我寻求的解决方案是你提供的那些环节之一,尤其是责任链——这几乎正是我试图实现的模式。
4赞 Aiden4 2/18/2021 #2

问题不在于词法生存期,添加显式不会更改错误。问题在于 - 强制在其整个生命周期内被借用,使其在借用被放弃后变得毫无用处。根据下面的评论,在一生中这样做基本上是不可能的。我建议改用一个盒子。将结构更改为drop&'a mut Chain<'a>root

pub enum Chain{
Root {
        value: String,
    },
    Child {
        parent: Box<Chain>,
    },
}

并根据需要调整其他方法。或者,如果您希望原始文件在不消耗自身的情况下保持可用,请使用。Rc<RefCell<Chain>>

评论

0赞 Aiden4 2/18/2021
@kmdreko你是对的,那对我来说不是很聪明。固定。
0赞 rmeador 2/19/2021
谢谢。我也许可以重构我的设计以使用 ,但我经常在链的末尾推送和弹出这些 Child 对象,因此很难考虑它是否归 Root 所有。Box
2赞 Aiden4 2/19/2021
@rmeador 可能值得一试。允许您共享所有权,并允许您动态而不是静态地强制执行 RAII。如果你这样做,只是要小心参考周期。Rc<RefCell<Chain>>RcRefCell