Search code examples
rustborrow-checker

Why does Rust's borrow checker complain when using an iterator returned from a method, but not when using a Vec's iterator directly?


https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ba9255e504174afa0f31f20467caf0a9

Consider the following Rust program

struct Foo {
    bar: Vec<i32>,
    baz: i32
}

impl Foo {

    fn get_an_iter(&self) -> impl Iterator<Item=&i32> {
        self.bar.iter()    
    }
    
    fn do_mut_stuff_works(&mut self) {
        for num in self.bar.iter() {
            self.baz += num;
        }
    }
    
    fn do_mut_stuff_does_not_work(&mut self) {
        for num in self.get_an_iter() {
            self.baz += num;
        }
    }
}

In do_mut_stuff_works I am iterating over the vector bar and change the field baz. To do this, the method needs to take &mut self.

Now if I move the creation of the iterator from inline to a method get_an_iter, and try to iterate over that in do_mut_stuff_does_not_work, the borrow checker complains.

How is it that, when using get_an_iter, the immutable borrow stays alive for the whole loop such that my mutable borrow at self.baz += num fails, but when I just create the iterator inside the method, with for num in self.bar.iter(), the borrow checker is happy?

The full compiler output is:

Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `self.baz` as mutable because it is also borrowed as immutable
  --> src/lib.rs:20:13
   |
19 |         for num in self.get_an_iter() {
   |                    ------------------
   |                    |
   |                    immutable borrow occurs here
   |                    immutable borrow later used here
20 |             self.baz += num;
   |             ^^^^^^^^^^^^^^^ mutable borrow occurs here

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

Solution

  • Rust can split borrows among fields. In do_mut_stuff_works, you start with a mutable borrow of self. When you take self.bar.iter(), that borrow is split: self.bar is immutably borrowed for the duration of the for loop. During this time, the mutable borrow of self as a whole is invalid, but the mutable borrow of self.baz remains. Thus you can increment self.baz in the body.

    In do_mut_stuff_does_not_work, you make an immutable borrow to the whole of self and give it to get_an_iter, so that borrow of self lasts for the whole for loop. Rust does not look into the definitions of functions to split borrows; that would be leaking implementation details about the function that aren't in the signature. So even though get_an_iter only needs an immutable borrow of self.bar, do_mut_stuff_does_not_work only knows that it has lent the entirety of self, immutably, to get_an_iter. Thus there is no mutable borrow of self.baz in the body of the loop that would justify incrementing it.