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

问题描述 投票:0回答:2

在我期望可变借用结束之后,我遇到了关于同时使用可变借用和不可变借用的令人困惑的错误。我对类似问题做了很多研究(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 borrow-checker
2个回答
19
投票

简而言之,

&'a mut Chain<'a>
具有极大的局限性和普遍性。

对于不可变引用

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

因此,当编译器尝试协调引用和参数链接时的生命周期

&'a 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
作为自参数,确保引用的生命周期不会比
scope
给出的生命周期短。
在内部,标准库包含此文档(

):

'scope

的不变性,以确保

'scope
不会收缩,这是稳健性所必需的。
如果没有不变性,这会编译得很好,但不健全:

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
,那么这将不起作用,并且在尝试生成多个线程时会遇到与上述相同的问题。
    


4
投票
drop

不会改变错误。问题在于

&'a mut Chain<'a>
- 迫使
root
在其整个生命周期中被借用,在借用被放弃后使其变得毫无用处。根据下面的评论,用生命周期来做到这一点基本上是不可能的。我建议用盒子代替。将结构更改为
pub enum Chain{
    Root {
        value: String,
    },
    Child {
        parent: Box<Chain>,
    },
}

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

    

© www.soinside.com 2019 - 2024. All rights reserved.