d0c_s4vage
d0c_s4vage

Reputation: 4077

How do I modify a Vector based on information from an item in the Vector?

How can I modify a Vec based on information from an item within the Vec without having both immutable and mutable references to the vector?

I've tried to create a minimal example that demonstrates my specific problem. In my real code, the Builder struct is already the intermediate struct that other answers propose. Specifically, I do not think this question is answered by other questions because:

Suppose I have a list of item definitions, where items are either a String, a nested list of Items, or indicate that a new item should be added to the list of items being processed:

enum Item {
    Direct(String),
    Nested(Vec<Item>),
    New(String),
}

There is also a builder that holds a Vec<Item> list, and builds an item at the specified index:

struct Builder {
    items: Vec<Item>,
}

impl Builder {
    pub fn build_item(&mut self, item: &Item, output: &mut String) {
        match item {
            Item::Direct(v) => output.push_str(v),
            Item::Nested(v) => {
                for sub_item in v.iter() {
                    self.build_item(sub_item, output);
                }
            }
            Item::New(v) => self.items.push(Item::Direct(v.clone())),
        }
    }

    pub fn build(&mut self, idx: usize, output: &mut String) {
        let item = self.items.get(idx).unwrap();
        self.build_item(item, output);
    }
}

This doesn't compile due to the error:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:26:9
   |
25 |         let item = self.items.get(idx).unwrap();
   |                    ---------- immutable borrow occurs here
26 |         self.build_item(item, output);
   |         ^^^^^----------^^^^^^^^^^^^^^
   |         |    |
   |         |    immutable borrow later used by call
   |         mutable borrow occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.

I don't know how to have the Builder struct be able to modify its items (i.e. have a mutable reference to self.items) based on information contained in one of the items (i.e. an immutable borrow of self.items).

Here is a playground example of the code.

Using Clone

@Stargateur recommended that I try cloning the item in build(). While this does work, I've been trying to not clone items for performance reasons. UPDATE: Without adding the Vec<Item> modification feature with Item::New, I implemented the clone() method in my real code and cloned the value in the equivalent of the example build() method above. I saw a 12x decrease in performance when I do self.items.get(idx).unwrap().clone() vs self.items.get(idx).unwrap(). I will continue looking for other solutions. The problem is, I'm still relatively new to Rust and am not sure how to bend the rules/do other things even with unsafe code.

Code that does work (playground)

impl Clone for Item {
    fn clone(&self) -> Self {
        match self {
            Item::Direct(v) => Item::Direct(v.clone()),
            Item::Nested(v) => Item::Nested(v.clone()),
            Item::New(v) => Item::New(v.clone()),
        }
    }
}

and change build to clone the item first:

        let item = self.items.get(idx).unwrap().clone();

Upvotes: 3

Views: 517

Answers (1)

EvilTak
EvilTak

Reputation: 7579

Whenever approaching problems like this (which you will encounter relatively frequently while using Rust), the main goal should be to isolate the code requiring the immutable borrow from the code requiring the mutable borrow. If the borrow from the items vec in build is unavoidable (i.e. you cannot move the item out of self.items or copy/clone it) and you must pass in a reference to this item to build_item, you might want to consider rewriting your build_item function to not mutate self. In this case, build_item only ever appends new items to the end of self.items, which lets us make an interesting refactor: Rather than have build_item modify items, make it return the items to be added to the original vector, and then have the caller add the newly generated items to the items vector.

impl Builder {
    fn generate_items(&self, item: &Item, output: &mut String) -> Vec<Item> {
        match item {
            Item::Direct(v) => {
                output.push_str(v);
                Vec::new()
            }
            Item::Nested(v) => {
                v.iter()
                    .flat_map(|sub_item| self.generate_items(sub_item, output))
                    .collect()
            }
            Item::New(v) => vec![Item::Direct(v.clone())],
        }
    }

    pub fn build_item(&mut self, item: &Item, output: &mut String) {
        let mut new_items = self.generate_items(item, output);
        self.items.append(&mut new_items);
    }

    pub fn build(&mut self, idx: usize, output: &mut String) {
        // Non lexical lifetimes allow this to compile, as the compiler
        // realizes that `item` borrow can be dropped before the mutable borrow

        // Immutable borrow of self starts here
        let item = self.items.get(idx).unwrap();
        let mut new_items = self.generate_items(item, output);
        // Immutable borrow of self ends here

        // Mutable borrow of self starts here
        self.items.append(&mut new_items);
    }
}

Note that in order to preserve the API, your build_item function has been renamed to generate_items, and a new build_item function that uses generate_items has been created.

If you look carefully, you'll notice that generate_items doesn't even require self, and can be a free-standing function or a static function in Builder.

Playground

Upvotes: 3

Related Questions