DEV Community

loading...
Cover image for Demystifying Mutability and References in Rust

Demystifying Mutability and References in Rust

arunanshub profile image Arunanshu Biswas ・4 min read

The Un-Sandwiching rule

All references and reference related problems follow the "un-sandwiching"
rule.

The "un-sandwiching" rule states that:

Given a value, you cannot use a mutable reference between an immutable reference's declaration "zone" and immutable reference's usage "zone". Also, you cannot use mutable or immutable reference between a mutable's declaration zone and mutable's usage zone.

Note:

"Un-Sandwiching rule" is just a made up name that I created to better explain the quirks of immutable and mutable references. Don't use this name in forums!

What does it mean?

This rule is a mashed and simplified form of:

At any given time, you can have either one mutable reference or any number of immutable references. References must always be valid.

(I agree that the "un-sandwiching" rule looks more lengthy, but it will help
you to simplify things.)

Explaination of the rule

The rule can be broken down into two parts.

  1. You cannot use a mutable reference while you are between the immutable reference's declaration and usage zone. However, you may have unlimited immutable references of the same value.

  2. You cannot have any reference (immutable or mutable), if you are between a mutable reference's declaration and usage zone.

A demonstration

fn main() {
    let mut a = String::from("hello world");

    let immut_ref = &a; // <- declaration zone
    /*


       This is the "un-sandwiching" zone. You can have unlimited immutable
       references to `a`, directly and indirectly (more on that below), but
       *NOT A SINGLE MUTABLE REFERENCE IS ALLOWED*.


    */
    println!("{}", immut_ref); // <- usage zone
}
Enter fullscreen mode Exit fullscreen mode

What is allowed?

You may have these types of references between that zone (not an exhaustive list):

    let b = &a; // (direct) *immutable* reference to `a`

    let c = &b; // (indirect) *immutable* reference to `b` (as `b` is immutable too)

    let d = &(*b); // (indirect) *immutable* reference to `a` via `b`
Enter fullscreen mode Exit fullscreen mode

What is not allowed?

These (or any variations of these) are illegal:

    let b_mut = &mut a; // You can see why. It clearly violates the "un-sandwiching"
                        // rule as we are creating a mutable reference between the
                        // declaration and usage zone.

    a.push_str("breeze"); // Cannot do this because `a` is an "immutable" now.
                          // More on this below.
Enter fullscreen mode Exit fullscreen mode

Let us look at the a.push_str("breeze") statement at a more granular level.

The signature of push_str is:

fn push_str(&mut self, string: &str) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Can you see what's going on here?

In the signature, we have a &mut self, which basically means &mut a (since self is a). But going back to the "un-sandwiching" rule, we see that we cannot have a mutable reference in the declaration-usage zone.

What do I mean by a becomes "immutable"?

I say a becomes "immutable" because you cannot mutate a as long as you are in the "un-sandwiching" zone. You can only have multiple immutable references.

What if we have multiple immutable references?

Let's say we have:

fn main() {
    let mut a = String::from("great breeze");

    let b = &a;
    // use `b`
    // use some more
    // ...

    let c = &a;
    // use `c`
    // use `b`
    // ...

    let d = &c;
    println!("{}", b); // last usage of `b`.
    // use `d`
    // use `c`

    do_something(d); // last usage of `d`
    // use `c`
    // ...

    do_something_else(c); // last usage of `c`
}
Enter fullscreen mode Exit fullscreen mode

Whew! That's a lot of jumbled references! Let's see this through our declaration-usage lens.

b
│
│
│      c
│      │
│      │
▼      │      d
~b     │      │
       │      │
       │      │
       │      ▼
       │      ~d
       │
       │
       ▼
       ~c
Enter fullscreen mode Exit fullscreen mode

The starting points are declarations, and the ~s are the last usage of the references. Note that we don't care if a reference is a direct or an indirect one (eg. d is an indirect reference).

From this diagram, we can clearly see that the "un-sandwiching" zone ranges from the first immutable reference b, to the last usage of immutable reference c. Between b and ~c, you cannot have any mutable reference.

And what about moves?

    let mut a = String::from("some");
    let c = &a;

    let other = a; // is this possible?

    println!("{}", c);
Enter fullscreen mode Exit fullscreen mode

This is very simple to answer.

If the object cannot be copied, then you cannot perform a move within the zone.

Your homework

Deduce whether the following code is possible or not:

fn main() {
    let mut a = String::from("mutable");
    let b = &mut a; // a *mutable* reference

    // ...

    let c = &a; // 1. Is this possible?

    // ...

    println!("{}", b); // last usage
}
Enter fullscreen mode Exit fullscreen mode

And what about this?

fn main() {
    let mut a = String::from("mutable");
    let b = &mut a; // a *mutable* reference

    // ...

    let d = &mut a; // 2. Is this possible?

    // ...

    println!("{}", b); // last usage
}
Enter fullscreen mode Exit fullscreen mode

Explaination

  1. let c = &a is not possible.

By the "un-sandwiching" rule we cannot have a immutable reference while we are within a mutable reference's "un-sandwiching" zone.

  1. let d = &mut a is not possible.

You can see why. We are trying to create a mutable reference of the value while we are within the mutable reference's "un-sandwiching" zone. This is not allowed.

Conclusion

I hope this article helps you in understanding Rust a bit better.

Questions? Comments? Concerns? Please put them down below and I'd be happy to help you.

Image Source: Manjaro's /usr/share/backgrounds folder 😃

Discussion (0)

Forem Open with the Forem app