Return reference to member variable from method - `cannot move out of borrowed content`


#1

I’m once again struggling with the borrow checker I think. I’m writing a simple archiver that writes output to hourly log files (eg: /logs/2016/Jun/03/11). To do this, I have a struct named FileArchiver, with the following definition:

#[derive(Debug)]
pub struct FileArchiver {
    base_path: String,
    current_file: Option<File>,
    current_date: DateTime<Local>,
}

impl FileArchiver {
    pub fn new(base_path: &str) -> Self {
        FileArchiver {
            base_path: base_path.to_string(),
            current_file: None,
            current_date: Local::now(),
        }
    }
}

When writing an event, the archiver checks if the hour of the current date/time matches the hour of the previous (current_date). If not, it closes the existing file and opens/creates a new file. This logic is handled by a method resolve_file_path. I managed to get this to work, returning a Result<(), Error>, but would rather return a Result<File, Error> to make the code cleaner. Here’s the code I’d like to use:

#[derive(Debug)]
pub struct FileArchiver {
    base_path: String,
    current_file: Option<File>,
    current_date: DateTime<Local>,
}

const NEW_LINE: &'static [u8] = b"\n";

impl FileArchiver {
    pub fn new(base_path: &str) -> Self {
        FileArchiver {
            base_path: base_path.to_string(),
            current_file: None,
            current_date: Local::now(),
        }
    }

    fn resolve_file_path(&mut self) -> Result<File, Error> {
        let current_time: DateTime<Local> = Local::now();

        // Check if the hour for the previous date matches the current hour.
        // If it doesn't, we need to set the new date and open a new file handle.
        if current_time.hour() != self.current_date.hour() {
            self.current_date = current_time;

            let mut file_path = PathBuf::from(&self.base_path);
            file_path.push(format!("{}", current_time.year()));
            file_path.push(format!("{}", current_time.month()));
            file_path.push(format!("{}", current_time.day()));
            file_path.push(format!("{}", current_time.hour()));
            file_path.set_extension("txt");

            if let Some(dir) = file_path.parent() {
                if !dir.exists() {
                    try!(fs::create_dir_all(dir));
                }
            }

            self.current_file = Some(try!(OpenOptions::new()
                                              .write(true)
                                              .append(true)
                                              .create(true)
                                              .open(file_path)));
        }

        Ok(self.current_file.unwrap())
    }

    pub fn write_event(&mut self, event: &[u8]) {
        match self.resolve_file_path() {
            Ok(ref mut f) => {
                match f.write(event) {
                    Ok(_) => {}
                    Err(e) => {
                        error!("Error writing to file: {}", e);
                    }
                };

                match f.write(NEW_LINE) {
                    Ok(_) => {}
                    Err(e) => {
                        error!("Error writing to file: {}", e);
                    }
                }
            }
            Err(e) => {
                warn!("Error resolving file path: {}", e);
            }
        };
    }
}

This results in:

src\file_archiver.rs:60:12: 60:16 error: cannot move out of borrowed content [E0507]
src\file_archiver.rs:60         Ok(self.current_file.unwrap())
                                   ^~~~
src\file_archiver.rs:60:12: 60:16 help: run `rustc --explain E0507` to see a detailed explanation
error: aborting due to previous error

This seems to be because self is borrowed and I now cannot move out of that. Is there any (easy) way around this, or am I better changing the method to return a Result<(), Error> and then do pattern matching on the Option in write_event?


#2

The way I see it, the problem is that you move a File into self.current_file and then immediately move it out again – which, as you pointed out, you cannot do.

Would it maybe work to instead return a Result(&mut File, Error), with the returned reference’s lifetime bound by the lifetime of &mut self?


#3

Yep, I tried returning &File, with an explicit lifetime:

    fn resolve_file_path<'a>(&'a mut self) -> Result<&File, Error> {
        let current_time: DateTime<Local> = Local::now();

        // Check if the hour for the previous date matches the current hour.
        // If it doesn't, we need to set the new date and open a new file handle.
        if current_time.hour() != self.current_date.hour() {
            self.current_date = current_time;

            let mut file_path = PathBuf::from(&self.base_path);
            file_path.push(format!("{}", current_time.year()));
            file_path.push(format!("{}", current_time.month()));
            file_path.push(format!("{}", current_time.day()));
            file_path.push(format!("{}", current_time.hour()));
            file_path.set_extension("txt");

            if let Some(dir) = file_path.parent() {
                if !dir.exists() {
                    try!(fs::create_dir_all(dir));
                }
            }

            self.current_file = Some(try!(OpenOptions::new()
                                              .write(true)
                                              .append(true)
                                              .create(true)
                                              .open(file_path)));
        }

        Ok(&self.current_file.unwrap())
    }

The error in that case is:

src\file_archiver.rs:54:13: 54:39 error: borrowed value does not live long enough
src\file_archiver.rs:54         Ok(&self.current_file.unwrap())
                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~
src\file_archiver.rs:26:68: 55:6 note: reference must be valid for the lifetime 'a as defined on the block at 26:67...
src\file_archiver.rs:26     fn resolve_file_path<'a>(&'a mut self) -> Result<&File, Error> {
src\file_archiver.rs:27         let current_time: DateTime<Local> = Local::now();
src\file_archiver.rs:28
src\file_archiver.rs:29         // Check if the hour for the previous date matches the current hour.
src\file_archiver.rs:30         // If it doesn't, we need to set the new date and open a new file handle.
src\file_archiver.rs:31         if current_time.hour() != self.current_date.hour() {
                        ...
src\file_archiver.rs:26:68: 55:6 note: ...but borrowed value is only valid for the block at 26:67
src\file_archiver.rs:26     fn resolve_file_path<'a>(&'a mut self) -> Result<&File, Error> {
src\file_archiver.rs:27         let current_time: DateTime<Local> = Local::now();
src\file_archiver.rs:28
src\file_archiver.rs:29         // Check if the hour for the previous date matches the current hour.
src\file_archiver.rs:30         // If it doesn't, we need to set the new date and open a new file handle.
src\file_archiver.rs:31         if current_time.hour() != self.current_date.hour() {
                        ...
src\file_archiver.rs:54:13: 54:17 error: cannot move out of borrowed content [E0507]
src\file_archiver.rs:54         Ok(&self.current_file.unwrap())
                                    ^~~~
src\file_archiver.rs:54:13: 54:17 help: run `rustc --explain E0507` to see a detailed explanation
error: aborting due to 2 previous errors

#4

Yes you get that error because unwrap:

moves the value v out of the Option if it is Some(v).

And as the compiler points out, well you cannot because all you have is a mutable reference to unwrap_file. If you really want to move the file out of unwrap_file, you can use take which will take the File out of the option. Or to return a mutable reference to an option’s inner data use as_mut().unwrap().

Alternatively, I prefer to pass a closure that takes a &mut Write, which is more generic than a file. Something like

use std::fs::File;
use std::io::{Read, Write};

fn write_to_file<F, W>(writer: &mut W, closure: F) where F: Fn(&mut W), W: Write {
    closure(writer);
}

fn main() {
  {
    let mut file = File::create("/tmp/output").unwrap();
    write_to_file(&mut file, |writer| {
      let _ = writer.write(&[65, 66, 67, 68]);
    });
  }
  
  let mut file = File::open("/tmp/output").unwrap();
  let mut contents = String::new();
  file.read_to_string(&mut contents).unwrap();
  println!("contents: {}", contents);
}