为什么 Rust 编译器在将变量移动到作用域后会出现“不能作为不可变借用,因为它也被作为可变借用”的错误?

时间:2021-05-26 20:50:55

标签: rust reference mutable

在阅读了 Rust 的作用域和引用之后,我编写了一个简单的代码来测试它们。

fn main() {
    // 1. define a string
    let mut a = String::from("great");

    // 2. get a mutable reference
    let b = &mut a;
    b.push_str(" breeze");
    println!("b = {:?}", b);

    // 3. A scope: c is useless after exiting this scope
    {
        let c = &a;
        println!("c = {:?}", c);
    }

    // 4. Use the mutable reference as the immutable reference's scope
    //    is no longer valid.
    println!("b = {:?}", b);  // <- why does this line cause an error?
}

据我所知:

  • 不可同时使用不可变和可变。
  • 不能存在同一对象的 2 个可变变量。
    • 但作用域可以允许存在 2 个可变变量,只要 2 个可变变量不在 相同的范围。

期待

3 中,c 是在一个范围内创建的,并且其中没有使用任何可变参数。因此, 当 c 超出范围时,很明显 c 将不再使用(如 它是无效的),因此 b 是一个可变引用,可以安全地用于 4

预期输出:

b = "great breeze"
c = "great breeze"
b = "great breeze"

现实

Rust 产生以下错误:

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/main.rs:12:17
   |
6  |     let b = &mut a;
   |             ------ mutable borrow occurs here
...
12 |         let c = &a;
   |                 ^^ immutable borrow occurs here
...
18 |     println!("b = {:?}", b); // <- why does this line cause an error?
   |                          - mutable borrow later used here

推理

(这只是我认为正在发生的事情,可能是谬论)

似乎无论如何都不能使用任何可变引用(b) 一旦创建了一个不可变的引用(c)(或被 Rust 编译器),无论是否在范围内。

这很像:

<块引用>

在任何给定时间,您都可以拥有一个可变引用或任何 不可变引用的数量。

这种情况类似于:

let mut a = String::from("great");

let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);
// ^ b's "session" ends here. Using 'b' below will violate the rule:
// "one mutable at a time"

// can use multiple immutables: follows the rule
// "Can have multiple immutables at one time"
let c = &a;
let d = &a;
println!("c = {:?}, d = {:?}", c, d);
println!("b = {:?}", b); // !!! Error

另外,只要我们使用不可变引用,原始对象 或引用变得“不可变”。如Rust Book所述:

<块引用>

当我们有一个不可变的引用时,我们不能有一个可变的引用。 不可变引用的用户不希望这些值突然出现 从他们下面变出来!

<块引用>

...你可以有任何一个可变引用...

let mut a = String::from("great");

let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);

let c = &b; // b cannot be changed as long as c is in use
b.push_str(" summer"); // <- ERROR: as b has already been borrowed.
println!("c = {:?}", c); // <- immutable borrow is used here

所以上面的这段代码在某种程度上解释了@Shepmaster 的解决方案。

回到原始代码并移除作用域:

// 1. define a string
let mut a = String::from("great");

// 2. get a mutable reference
let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);

// 3. No scopes here.
let c = &a;
println!("c = {:?}", c);


// 4. Use the mutable reference as the immutable reference's scope
//    is no longer valid.
println!("b = {:?}", b);  // <- why does this line cause an error?

现在很清楚为什么这段代码有错误了。 Rust 编译器看到 我们使用的是可变的 b(它是 a 的可变引用, 因此 a 变得不可变)同时还借用了不可变的 参考 c。我喜欢称其为“中间没有不可变因素”。

或者我们也可以称之为“非夹心”。你不能拥有/使用可变的 介于“不可变声明”和“不可变使用”之间,反之亦然。

但这仍然没有回答为什么范围在这里失败的问题。

问题

  • 即使在明确地将 c 移入作用域之后,为什么 Rust 编译器生成此错误消息?

1 个答案:

答案 0 :(得分:4)

您的问题是为什么编译器不允许 c 引用已经可变借用的数据。我希望一开始就不允许这样做!

但是 - 当您注释掉最后一个 println!() 时,代码可以正确编译。大概这就是导致您得出允许别名的结论的原因,“只要可变参数不在同一范围内”。我认为这个结论是不正确的,原因如下。

虽然在某些情况下确实允许子作用域中的引用使用别名,但它需要进一步的限制,例如通过结构体投影缩小现有引用。 (例如,给定一个 let r = &mut point,您可以编写 let rx = &mut r.x,即临时可变地借用可变借用数据的子集。)但这里不是这种情况。这里的 c 是对已由 b 可变引用的数据的全新共享引用。这绝不应该被允许,但它可以编译。

答案在于编译器对 non-lexical lifetimes (NLL) 的分析。当您注释掉最后一个 println!() 时,编译器会注意到:

  1. b 在第一个 Drop 之后不再使用。

因此 NLL 在第一个 b 之后插入一个不可见的 println!(),从而允许首先引入 drop(b)。只是因为隐含的 println!() c 不会创建可变别名。换句话说,drop(b) 的范围被人为地缩短了由纯词法分析确定的范围(它相对于 cb 的位置),因此非词法生命周期。

您可以通过将引用包装在新类型中来测试此假设。例如,这相当于您的代码,它仍然在编译时将最后一个 { 注释掉:

}

但是,如果我们仅仅为 println!() 实现 #[derive(Debug)] struct Ref<'a>(&'a mut String); fn main() { let mut a = String::from("great"); let b = Ref(&mut a); b.0.push_str(" breeze"); println!("b = {:?}", b); { let c = &a; println!("c = {:?}", c); } //println!("b = {:?}", b); } ,代码将不再编译:

Drop

明确回答您的问题:

<块引用>

即使将 Ref 显式移动到作用域中,为什么 Rust 编译器会产生此错误消息?

因为从一开始就不允许 // causes compilation error for code above impl Drop for Ref<'_> { fn drop(&mut self) { } } c 一起存在,无论是内部作用域。当它存在时,编译器可以证明 c 永远不会与 b 并行使用并且在 b 之前删除它是安全的甚至构建。在这种情况下,别名是“允许的”,因为尽管 c “在范围内”,但没有实际别名 - 在生成的 MIR/HIR 级别上,只有 c 引用了数据。