Borrow check understanding

Hi,
I'm using zip crate

    let path = "/tmp/a.jar";
    let jar = File::open(path).unwrap();
    let mut zip = ZipArchive::new(jar).unwrap();

    for name in zip.file_names() {
        let entry = zip.by_name(name);
        println!("name: {}, size: {}", name, entry.unwrap().size());
    }

This will complains

However this will pass

    let path = "/tmp/a.jar";
    let jar = File::open(path).unwrap();
    let mut zip = ZipArchive::new(jar).unwrap();

    for i in 0..zip.len() {
        let entry = zip.by_index(i).unwrap();
        println!("name: {}, size: {}", entry.name(), entry.size());
    }
    pub fn len(&self) -> usize {
        self.shared.files.len()
    }
    pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
        Ok(self
            .by_index_with_optional_password(file_number, None)?
            .unwrap())
    }
    pub fn file_names(&self) -> impl Iterator<Item = &str> {
        self.shared.names_map.keys().map(|s| s.as_str())
    }
    /// Search for a file entry by name
    pub fn by_name<'a>(&'a mut self, name: &str) -> ZipResult<ZipFile<'a>> {
        Ok(self.by_name_with_optional_password(name, None)?.unwrap())
    }

You can see, len() and file_names all borrow immutable, and by_name by_index all borrow mutable. Why the second can pass the borrow check, what makes the difference ?

Thanks,
Aitozi.

An iterator like file_names will (need to) keep its borrow for the whole duration of the iteration. This is why calling a &mut self method like .by_name inside of the loop won’t work. What it also will do is link the borrow given to fn file_names(&self, …) to the lifetime of the returned file name &str s, so even after the loop you can only ever pass the name back into by_name if you were to copy it into an owned String.

The 0..zip.len() iterator does not keep any borrow of zip alive. Even though .len is also a &self method, it returns a usize that has nothing to do with the &self borrow passed into len after the method returns. This is why after creating this iterator, and thus during iteration, you can access zip freely, even by mutable reference, as by_index requries.


The way the type signatures express this distinction between zip.file_names() and 0..zip.len() is a bit tricky, as there’s multiple implicit convenience features at play: lifetime elision on one hand, and special rules of how impl SomeTrait return types interact with lifetimes on the other hand.

The signature fn len(&self) -> usize is quite basic. It has simple lifetime elision and stands for a generic signature like fn len<'a>(&'a self) -> usize. The return type usize has no lifetimes and is thus considered fully owned and independent of any other borrows or references, in particular the borrow checker does not relate it in any way to the&'a self reference that was put in.

The signature fn file_names(&self) -> impl Iterator<Item = &str> stands for, via lifetime elision, the generic signature fn file_names<'a>(&'a self) -> impl Iterator<Item = &'a str>. Then as for the meaning of impl Iterator… it stands for some opaque type, let’s call it FileNamesIterator with certain generic arguments. For the purpose of understanding what the generic arguments are, we must incorporate also the generics of the impl block in question

impl<R: Read + Seek> ZipArchive<R> {
    fn file_names<'a>(&'a self) -> impl Iterator<Item = &'a str> {…}
}

The opaque type will, according to some rules I could look up the RFC for but will not do at the moment, incorporate every generic type parameter present, which is R, and all lifetime parameters that are mentioned in the trait bound, which does include the only lifetime present, 'a, in this case. So you can think of the signature as fn file_names<'a>(&'a self) -> FileNamesIterator<'a, R>. The opaque return type FileNamesIterator<'a, R> does have a lifetime parameter, and the file_names method’s function signature relates (i.e. equates) it to the &'a self reference’s lifetime, which has the effect that as long as the iterator still exits, the original &'a self reference must be kept alive, and the borrow checker will enforce this. Furthermore, the items of the iterator are of type &'a str for this same lifetime, so keeping any of these &str items would also keep the original &'a self reference of the self.file_names() call alive, and thus would have the borrow checker still consider self borrowed while any of those name items are around.

2 Likes

It is a fundamental requirement of memory safety that shared mutability is forbidden. While you are mutating a value through a &mut reference, you are not allowed to have any other reference to it, and while you are observing a value through one or more & references, you are not allowed to mutate it.

Thanks for your reply and @steffahn 's detailed analysis. It helps me out

I think as you mentioned function file_names's lifetime already enforce this

fn file_names<'a>(&'a self) -> impl Iterator<Item = &'a str> {…}

Because the result iterator hold the reference, so the self immutable reference should not shorter than the return iterator. So in the follow up loop, the immutable reference is still in effect. This will be conflict with the mutable reference in the loop

As a workaround, you can store the list of file names in a Vec<String> if you don't mind the memory allocation:

let file_names: Vec<String> = zip.file_names().map(str::to_owned).collect();

for file_name in &file_names {
    let entry = zip.by_name(file_name);
    // ...
}

Of course, traversing by index is a more straightforward option.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.