Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to workaround the coexistence of a mutable and immutable borrow?

I have a Context struct:

struct Context {
    name: String,
    foo: i32,
}

impl Context {
    fn get_name(&self) -> &str {
        &self.name
    }
    fn set_foo(&mut self, num: i32) {
        self.foo = num
    }
}

fn main() {
    let mut context = Context {
        name: "MisterMV".to_owned(),
        foo: 42,
    };
    let name = context.get_name();
    if name == "foo" {
        context.set_foo(4);
    }
}

In a function, I need to first get the name of the context and update foo according to the name I have:

let name = context.get_name();
if (name == "foo") {
    context.set_foo(4);
}

The code won't compile because get_name() takes &self and set_foo() takes &mut self. In other words, I have an immutable borrow for get_name() but I also have mutable borrow for set_foo() within the same scope, which is against the rules of references.

At any given time, you can have either (but not both of) one mutable reference or any number of immutable references.

The error looks like:

error[E0502]: cannot borrow `context` as mutable because it is also borrowed as immutable
  --> src/main.rs:22:9
   |
20 |     let name = context.get_name();
   |                ------- immutable borrow occurs here
21 |     if name == "foo" {
22 |         context.set_foo(4);
   |         ^^^^^^^ mutable borrow occurs here
23 |     }
24 | }
   | - immutable borrow ends here

I'm wondering how can I workaround this situation?

like image 433
xxks-kkk Avatar asked Oct 08 '18 19:10

xxks-kkk


2 Answers

This is a very broad question. The borrow checker is perhaps one of the most helpful features of Rust, but also the most prickly to deal with. Improvements to ergonomics are being made regularly, but sometimes situations like this happen.

There are several ways to handle this and I'll try and go over the pros and cons of each:

I. Convert to a form that only requires a limited borrow

As you learn Rust, you slowly learn when borrows expire and how quickly. In this case, for instance, you could convert to

if context.get_name() == "foo" {
    context.set_foo(4);
}

The borrow expires in the if statement. This usually is the way you want to go, and as features such as non-lexical lifetimes get better, this option gets more palatable. For instance, the way you've currently written it will work when NLLs are available due to this construction being properly detected as a "limited borrow"! Reformulation will sometimes fail for strange reasons (especially if a statement requires a conjunction of mutable and immutable calls), but should be your first choice.

II. Use scoping hacks with expressions-as-statements

let name_is_foo = {
    let name = context.get_name();
    name == "foo"
};

if name_is_foo {
    context.set_foo(4);
}

Rust's ability to use arbitrarily scoped statements that return values is incredibly powerful. If everything else fails, you can almost always use blocks to scope off your borrows, and only return a non-borrow flag value that you then use for your mutable calls. It's usually clearer to do method I. when available, but this one is useful, clear, and idiomatic Rust.

III. Create a "fused method" on the type

   impl Context {
      fn set_when_eq(&mut self, name: &str, new_foo: i32) {
          if self.name == name {
              self.foo = new_foo;
          }
      }
   }

There are, of course, endless variations of this. The most general being a function that takes an fn(&Self) -> Option<i32>, and sets based on the return value of that closure (None for "don't set", Some(val) to set that val).

Sometimes it's best to allow the struct to modify itself without doing the logic "outside". This is especially true of trees, but can lead to method explosion in the worst case, and of course isn't possible if operating on a foreign type you don't have control of.

IV. Clone

let name = context.get_name().clone();
if name == "foo" {
    context.set_foo(4);
}

Sometimes you have to do a quick clone. Avoid this when possible, but sometimes it's worth it to just throw in a clone() somewhere instead of spending 20 minutes trying to figure out how the hell to make your borrows work. Depends on your deadline, how expensive the clone is, how often you call that code, and so on.

For instance, arguably excessive cloning of PathBufs in CLI applications isn't horribly uncommon.

V. Use unsafe (NOT RECOMMENDED)

let name: *const str = context.get_name();
unsafe{
    if &*name == "foo" {
        context.set_foo(4);
    }
}

This should almost never be used, but may be necessary in extreme cases, or for performance in cases where you're essentially forced to clone (this can happen with graphs or some wonky data structures). Always, always try your hardest to avoid this, but keep it in your toolbox in case you absolutely have to.

Keep in mind that the compiler expects that the unsafe code you write upholds all the guarantees required of safe Rust code. An unsafe block indicates that while the compiler cannot verify the code is safe, the programmer has. If the programmer hasn't correctly verified it, the compiler is likely to produce code containing undefined behavior, which can lead to memory unsafety, crashes, etc., many of the things that Rust strives to avoid.

like image 151
Linear Avatar answered Nov 13 '22 04:11

Linear


There is problably some answer that will already answer you, but there is a lot of case that trigger this error message so I will answer your specific case.

The easier solution is to use #![feature(nll)], this will compile without problem.

But you could fix the problem without nll, by using a simple match like this:

fn main() {
    let mut context = Context {
        name: "MisterMV".to_owned(),
        foo: 42,
    };
    match context.get_name() {
        "foo" => context.set_foo(4),
        // you could add more case as you like
        _ => (),
    }
}
like image 4
Stargateur Avatar answered Nov 13 '22 04:11

Stargateur