Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
534 views
in Technique[技术] by (71.8m points)

reference - Why does the Rust compiler error with "cannot borrow as immutable because it is also borrowed as mutable" after moving the variable into a scope?

After reading about Rust's scopes and references, I wrote a simple code to test them.

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?
}

As far as I understand:

  • Immutables and mutables cannot be used simultaneously.
  • 2 mutables of same object cannot exist.
    • But scoping can allow 2 mutable to exist as long as 2 mutables are not in the same scope.

Expectation

In 3, c is created within a scope, and no mutables are used in it. Hence, when c goes out of scope, it is clear that c will not be used anymore (as it would be invalid) and hence b, a mutable reference, can be used safely in 4.

The expected output:

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

Reality

Rust produces the following error:

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

Inference

(This is just what I think is happening, and can be a fallacy)

It seems that no matter what, you cannot use any mutable references (b) once an immutable reference (c) was made (or "seen" by the rust compiler), whether in a scope or not.

This is much like:

At any given time, you can have either one mutable reference or any number of immutable references.

This situation is similar to:

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

Also, as long as we are using immutable references, the original object or reference becomes "immutable". As said in Rust Book:

We also cannot have a mutable reference while we have an immutable one. Users of an immutable reference don’t expect the values to suddenly change out from under them!

and

...you can have either one mutable reference...

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

So this code above somewhat explains @Shepmaster's solution.

Going back to the original code and removing the scope:

// 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?

Now it is clear why this code has an error. The rust compiler sees that we are using a mutable b (which is a mutable reference of a, therefore a becomes immutable) while also borrowing an immutable reference c. I like to call it "no immutables in between".

Or we can also call it "un-sandwiching". You cannot have/use a mutable between "immutable declaration" and "immutable use" and vice-versa.

But this still does not answer the question of why scopes fail here.

Question

  • Even after explicitly moving c into a scope, why does the Rust compiler produce this error message?
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Your question is why doesn't compiler allow c to refer to data which is already mutably borrowed. I for one would expect that to be disallowed to begin with!

But - when you comment out the very last println!(), the code compiles correctly. Presumably that's what led you to conclude that aliasing is allowed, "as long as mutables aren't in the same scope". I argue that that conclusion is incorrect, and here is why.

While it's true that there are some cases where aliasing is allowed for references in sub-scopes, it requires further restrictions, such as narrowing an existing reference through struct projection. (E.g. given a let r = &mut point, you can write let rx = &mut r.x, i.e. temporarily mutably borrow a subset of mutably borrowed data.) But that's not the case here. Here c is a completely new shared reference to data already mutably referenced by b. That should never be allowed, and yet it compiles.

The answer lies with the compiler's analysis of non-lexical lifetimes (NLL). When you comment out the last println!(), the compiler notices that:

  1. b isn't Drop, so no one can observe a difference if we pretend it was dropped sooner, perhaps immediately after last use.

  2. b is no longer used after the first println!().

So NLL inserts an invisible drop(b) after the first println!(), thereby allowing introduction of c in the first place. It's only because of the implicit drop(b) that c doesn't create a mutable alias. In other words, the scope of b is artificially shortened from what would be determined by purely lexical analysis (its position relative to { and }), hence non-lexical lifetime.

You can test this hypothesis by wrapping the reference in a newtype. For example, this is equivalent to your code, and it still compiles with the last println!() commented out:

#[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);
}

But, if we merely implement Drop for Ref, the code no longer compiles:

// causes compilation error for code above
impl Drop for Ref<'_> {
    fn drop(&mut self) {
    }
}

To explicitly answer your question:

Even after explicitly moving c into a scope, why does the Rust compiler produce this error message?

Because c is not allowed to exist alongside b to begin with, regardless of being an an inner scope. When it is allowed to exist is in cases where the compiler can prove that b is never used in parallel with c and it's safe to drop it before c is even constructed. In that case aliasing is "allowed" because there's no actual aliasing despite b "being in scope" - on the level of the generated MIR/HIR, it's only c that refers to the data.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...