Remove a value from a vec based on a reference to that value

ghz 8months ago ⋅ 67 views

I'm trying to write a method for a struct which has a Vec as a member. The function I'd like to write, remove_thing, should take a reference to an element of the Vec and remove it, leaving the rest in place.

I'm getting a borrow checker error, that I cannot borrow the Vec as mutable, because I have a borrow on the element as immutable. My expectation would be that there is still a valid way to express this, as I'm not actually mutating the interior element, just the containing Vec. But I could be wrong.

Here's a short reproduction:

struct Thing {
    name: String,
    data: String,
}

struct HasSomeThings {
    things: Vec<Thing>,
}

impl HasSomeThings {
    fn thing_by_name(&self, name: &str) -> Option<&Thing> {
        for t in self.things.iter() {
            if t.name == name {
                return Some(&t);
            }
        }
        None
    }

    // Is it impossible to implement this function? 
    // It seems having a reference to a Thing in the vector prevents 
    // you from removing it from that vector.    
    fn remove_thing(&mut self, thing: &Thing) {
        self.things.retain(|t| t.name != thing.name);
    }
}

fn main() {
    // Imagine this comes from reading a file somewhere
    let mut some_things = HasSomeThings {
        things: vec![
            Thing { name: "one".to_string(), data: "one data".to_string() },
            Thing { name: "two".to_string(), data: "two data".to_string() },
            Thing { name: "three".to_string(), data: "three data".to_string() },
        ]
    };
    
    // Imagine this comes from user input
    let name = "one";
    let one_thing = some_things.thing_by_name(name).unwrap();
    println!("{}", one_thing.data);
    
    some_things.remove_thing(one_thing);
}

Rust Playground Link

The error I'm seeing is:

Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `some_things` as mutable because it is also borrowed as immutable
  --> src/main.rs:44:5
   |
41 |     let one_thing = some_things.thing_by_name(name).unwrap();
   |                     ----------- immutable borrow occurs here
...
44 |     some_things.remove_thing(one_thing);
   |     ^^^^^^^^^^^^------------^^^^^^^^^^^
   |     |           |
   |     |           immutable borrow later used by call
   |     mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` (bin "playground") due to 1 previous error

Is there some trick I can do to implement the remove_thing function? Or is there a reasonable way to restructure my code so that I can both work with a reference to the element from the Vec and then later remove it? Do I just need to rebuild the whole HasSomeThings instance from scratch?

Answers

The issue you're encountering is due to Rust's borrow checker enforcing the ownership rules. When you call thing_by_name, you're borrowing self immutably. Later, when you try to call remove_thing, you're attempting to borrow self mutably while it's still borrowed immutably, which Rust doesn't allow.

To solve this, you can restructure your code slightly. Instead of passing a reference to the Thing to remove_thing, you can pass the index of the Thing to be removed. Since you're removing by name, you can first find the index of the Thing by name, then remove it by index.

Here's how you can do it:

impl HasSomeThings {
    fn thing_by_name(&self, name: &str) -> Option<usize> {
        self.things.iter().position(|t| t.name == name)
    }

    fn remove_thing(&mut self, index: usize) {
        self.things.remove(index);
    }
}

fn main() {
    let mut some_things = HasSomeThings {
        things: vec![
            Thing { name: "one".to_string(), data: "one data".to_string() },
            Thing { name: "two".to_string(), data: "two data".to_string() },
            Thing { name: "three".to_string(), data: "three data".to_string() },
        ]
    };

    let name = "one";
    if let Some(index) = some_things.thing_by_name(name) {
        let one_thing = &some_things.things[index];
        println!("{}", one_thing.data);
        some_things.remove_thing(index);
    }
}

In this version, thing_by_name returns an Option<usize> representing the index of the Thing with the given name. Then, in main, if a Thing with the given name is found, it's removed from the vector using its index. This way, you avoid borrowing self immutably and mutably at the same time, resolving the borrow checker error.