4
\$\begingroup\$

I am trying to learn basic Rust. Is this a good way to get the contents of a file? Am I handling errors the best way? Is there any way this could be more performant?

use std::fs::File;
use std::io::Read;

fn main() {
    let path = "bar/foo.txt";

    match read(path) {
        Ok(contents) => println!("{}", contents),
        Err(err) => println!("Unable to read '{}': {}", path, err),
    }
}

fn read(file: &str) -> Result<String, std::io::Error> {
    let mut file = match File::open(file) {
        Ok(mut f) => f,
        Err(err) => return Err(err),
    };

    let mut data = String::new();
    match file.read_to_string(&mut data) {
        Ok(_) => return Ok(data),
        Err(err) => return Err(err),
    }
}
\$\endgroup\$
3
\$\begingroup\$
  1. Your code gives a warning:

    warning: variable does not need to be mutable
      --> src/main.rs:15:12
       |
    15 |         Ok(mut f) => f,
       |            ^^^^^
       |
    

    When you move a value from one binding to another, the two bindings don't have to agree on mutability. It may sound strange, but it's valid to move from an immutable binding to a mutable binding. It's perfectly safe because in order to be able to move a value, there must not be any pointers to it, which ensures that you have exclusive access to the value. Therefore, you could write Ok(f) here instead of Ok(mut f).

  2. The match / return Err pattern is so frequent that Rust has a shorthand for it: the ? operator (and its predecessor, the try! macro). We could rewrite your program using the ? operator like this:

    use std::fs::File;
    use std::io::Read;
    
    fn main() {
        let path = "bar/foo.txt";
    
        match read(path) {
            Ok(contents) => println!("{}", contents),
            Err(err) => println!("Unable to read '{}': {}", path, err),
        }
    }
    
    fn read(file: &str) -> Result<String, std::io::Error> {
        let mut file = File::open(file)?;
    
        let mut data = String::new();
        file.read_to_string(&mut data)?;
        Ok(data)
    }
    

    The second match in read ends the function, so we don't need the ? operator. Instead, we could use map to replace the data in the Ok variant while keeping the Err the same, and then just return the result of map.

    fn read(file: &str) -> Result<String, std::io::Error> {
        let mut file = File::open(file)?;
    
        let mut data = String::new();
        file.read_to_string(&mut data).map(|_| data)
    }
    

    Which one you use is up to you. :)

  3. File::open accepts more than just &str. Its signature is:

    fn open<P: AsRef<Path>>(path: P) -> Result<File>
    

    You could make your own read function more generic by introducing a type parameter.

    use std::path::Path;
    
    fn read<P: AsRef<Path>>(file: P) -> Result<String, std::io::Error> {
        let mut file = File::open(file)?;
    
        let mut data = String::new();
        file.read_to_string(&mut data)?;
        Ok(data)
    }
    

    Look at the list of implementations for AsRef<Path> to see what types you can now pass to your function.

\$\endgroup\$
  • \$\begingroup\$ The reason for this apparently overcomplicated string handling is that filenames aren't required to be valid unicode while String/&str require valid unicode. \$\endgroup\$ – CodesInChaos Dec 20 '17 at 17:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.