soarjay
soarjay

Reputation: 651

Object oriented design patterns for error checking

I have written the following function that reads the contents of a text file and panic!s if an error is encountered.

fn get_file_contents(name: String) -> Result<String, io::Error> {
    let mut f = try!(File::open(name));
    let mut contents = String::new();
    try!(f.read_to_string(&mut contents));
    Ok(contents)
}

And the contents are extracted from the Result using:

let file_contents = match get_file_contents(file_name) {
    Ok(contents) => contents,
    Err(err) => panic!("{}", err)
};

I am now trying to reimplement this in an object oriented manner using structures and implementations. I created the following structure:

struct FileReader {
    file_name: String,
    file_contents: String,
}

and implemented the following methods:

impl FileReader {
    fn new(fname: &str) -> FileReader {
        FileReader {
            file_name: fname.to_string(),
            file_contents: String::new(),
        }
    }

    fn get_file_contents(&mut self) {
        let mut f = match File::open(&self.file_name) {
            Ok(file) => file,
            Err(err) => panic!("{}", err)
        };

        match f.read_to_string(&mut self.file_contents) {
            Ok(size) => size,
            Err(err) => panic!("{}", err)
        };
    }
}

In the OO approach, I haven't used the try! macro as I don't want the method to return any value. Is my OO implementation of get_file_contents a typical way of achieving this functionality? If not, can you please suggest an alternative way?

Upvotes: 0

Views: 157

Answers (1)

Shepmaster
Shepmaster

Reputation: 431299

In the OO approach, I haven't used the try! macro as I don't want the method to return any value.

It's unclear why you think that "object oriented" means "doesn't return a value". If an error can occur, the code should indicate that.

Many languages have the equivalent of exceptions — out of band values that are thrown (also known as "returned") from a function or method. Note that this means that these languages allow for two disjoint types to be returned from a given function: the "normal" type and the "exceptional" type. That is a close equivalent for Rust's Result: Result<NormalType, ExceptionalType>.

Exceptional isn't a great term for this, as you should expect that opening a file should fail. There's an infinite number of ways that it could not work, but only a narrow subset of ways that it can succeed.

Panicking is closer to "kill the entire program / thread right now". Unlike C, you are forced to either deal with a problem, pass it back to the caller, or kill the program (panic).

If you would have thrown an exception in a language that supports them, use a Result. If you would have killed the program, or don't want to handle an error, use a panic.


If you want to panic in your particular case, use unwrap, or even better, expect:

fn get_file_contents(&mut self) {
    let mut f = File::open(&self.file_name).expect("Couldn't open file");
    f.read_to_string(&mut self.file_contents).expect("Couldn't read file");
}

seems kind of clunky to have to deal with the Result for each method.

Which is why the Error Handling section of The Rust Programming Language spends a good amount of time discussing the try! macro:

A cornerstone of error handling in Rust is the try! macro. The try! macro abstracts case analysis like combinators, but unlike combinators, it also abstracts control flow. Namely, it can abstract the early return pattern seen above.

(this makes more sense in context of the page)

I don't want my code to try and recover from the error (most likely caused by the file not being found) - I want it to print a useful error message and then die

Then by all means, panic. There's more succinct AND more detailed ways to do it (as shown above).

Upvotes: 1

Related Questions