Borrow check understanding

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