Question Mark
Lots of library functions--- including the standard library, as well as third-party libraries--- return either options or results, so you'll likely get used to using things like match
, if let
, while let
, unwrap
, and expect
even before you make an Option
or Result
yourself.
Example: Open
For example, when we open a file in Perl, it returns an undefined value when it fails, so we usually write something like this
open my $fh, '<', $file or die "Cannot open file '$file': $!";
Upon success, our file handle is in $fh
. Upon failure, we retrieve the reason for the failure from $!
.
The similar maneuver in Rust
use std::fs::File; use std::path::Path; fn main() { let path = Path::new("no_such_file"); let result = File::open(&path); match result { Ok(f) => println!("The open succeeded\n{:#?}", f), Err(e) => println!("The open failed\n{:#?}", e), } }
gives us a Result<File, Error>
. Upon success, a std::fs::File is wrapped in an Ok
. Among other things, it contains the file descriptor we need to read from the file. Upon failure, a std::io::Error is wrapped in an Err
. Among other things, it contains the reason for the failure.
If we match on the result, we could inspect the File struct or the Error struct like so
use std::fs::File; use std::path::Path; fn main() { let path = Path::new("no_such_file"); let result = File::open(&path); match result { Ok(f) => println!("The open succeeded\n{:#?}", f), Err(e) => println!("The open failed\n{:#?}", e), } }
Again, we can call unwrap
or expect
on a Result
and it will panic if it is Err
, so perhaps a more direct analogue to
open my $fh, '<', $file or die "Cannot open file '$file': $!";
would be
use std::fs::File; use std::path::Path; fn main() { let path = Path::new("no_such_file"); let f = File::open(&path).expect("Cannot open file!"); println!("The open succeeded\n{:#?}", f); }
This unwraps our File on success and panics with our message on failure.
The ? (question mark) operator
Rust has a more idiomatic way of dealing with options and results. Perhaps this is best shown by example. Say we wanted to read the first eight bytes of a file (perhaps we want to check if it's a PNG file or something). We might write something like this
use std::fs::File; use std::io; use std::io::{Error, ErrorKind}; use std::io::prelude::*; use std::path::Path; fn main() { let path = Path::new("src/main.rs"); let first_eight = read_eight_bytes(&path); dbg!(&first_eight); } fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> { let result = File::open(path); let mut f = match result { Ok(f) => f, Err(e) => return Err(e), }; let mut buffer = [0; 8]; let result = f.read(&mut buffer[..]); let n = match result { Ok(n) => n, Err(e) => return Err(e), }; if n == 8 { Ok(buffer) } else { Err(Error::new(ErrorKind::Other, "Could not read 8 bytes!")) } }
We're given a path and we're returning a Result<[u8; 8], io::Error>
. That is, we're going to return Ok
with eight bytes or Err
with the reason we couldn't.
First, we open the path, which might fail. If it does, we return its io::Error. If it succeeds, we unwrap the File and try to read eight bytes from it. If that fails, we return its io::Error. If it succeeds, we check that we got eight bytes. If so, return them in an Ok. If not, return a custom error.
Both of those matches have the same shape: unwrap the Ok
or return early with the Err
. This is so common, that we can replace it with a single question mark!
use std::fs::File; use std::io; use std::io::{Error, ErrorKind}; use std::io::prelude::*; use std::path::Path; fn main() { let path = Path::new("dirk-gently.png"); let first_eight = read_eight_bytes(&path); dbg!(&first_eight); } fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> { let mut f = File::open(path)?; let mut buffer = [0; 8]; let n = f.read(&mut buffer[..])?; if n == 8 { Ok(buffer) } else { Err(Error::new(ErrorKind::Other, "Could not read 8 bytes!")) } }
Not bad, eh? We're doing all the proper error checking, but it's mostly just the "happy path" showing in our code.
Also, Result<T, io::Error>
is so common that there's a type alias for it, io::Result<T>
. That is, we could replace
#![allow(unused_variables)] fn main() { fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> { }
with
#![allow(unused_variables)] fn main() { fn read_eight_bytes(path: &Path) -> io::Result<[u8; 8]> { }
if we wanted.
Conclusion
It takes some getting used to perhaps, but Rust's error handling is really pretty nice. It may seem finicky, but at least we don't have to juggle undefined values or exceptions.