Item 3: Avoid match
ing Option
and Result
Item 1 expounded the virtues of enum
and showed how match
expressions force the programmer to take all
possibilities into account; this Item explores situations where you should prefer to avoid match
expressions –
explicitly at least.
Item 1 also introduced the two ubiquitous enum
s that are provided by the Rust standard library:
Option<T>
to express that a value (of typeT
) may or may not be presentResult<T, E>
, for when an operation to return a value (of typeT
) may not succeed, and may instead return an error (of typeE
).
For these particular enum
s, explicitly using match
often leads to code that is less compact than it needs to be,
and which isn't idiomatic Rust.
The first situation where a match
is unnecessary is when only the value is relevant, and the absence of value (and any
associated error) can just be ignored.
#![allow(unused)] fn main() { struct S { field: Option<i32>, } let s = S { field: Some(42) }; match &s.field { Some(i) => println!("field is {}", i), None => {} } }
For this situation, an if let
expression is one line shorter and, more importantly, clearer:
if let Some(i) = &s.field {
println!("field is {}", i);
}
However, most of the time the absence of a value, and an associated error, is going to be something that the programmer has to deal with. Designing software to cope with failure paths is hard, and most of that is essential complexity that no amount of syntactic support can help with – specifically, deciding what should happen if an operation fails.
In some situations, the right decision is to perform an ostrich manoeuvre and explicitly not cope with
failure. Doing this with an explicit match
would be needlessly verbose:
#![allow(unused)] fn main() { let result = std::fs::File::open("/etc/passwd"); let f = match result { Ok(f) => f, Err(_e) => panic!("Failed to open /etc/passwd!"), }; }
Both Option
and Result
provide a pair of methods that extract their inner value and panic!
if it's absent:
unwrap
and
expect
. The latter allows the error
message on failure to be personalized, but in either case the resulting code is shorter and simpler – error
handling is delegated to the .unwrap()
suffix (but is still present).
#![allow(unused)] fn main() { let f = std::fs::File::open("/etc/passwd").unwrap(); }
Be clear, though: these helper functions still panic!
, so choosing to use them is the same as choosing to panic!
(Item 18).
However, in many situations, the right decision for error handling is to defer the decision to somebody else. This is
particularly true when writing a library, where the code may be used in all sorts of different environments that can't
be foreseen by the library author. To make that somebody else's job easier, prefer Result
to Option
, even
though this may involve conversions between different error types (Item 4).
Result
has also a
[#must_use]
attribute to nudge library users in the right direction – if the code using the returned Result
ignores it, the
compiler will generate a warning:
warning: unused `Result` that must be used
--> transform/src/main.rs:32:5
|
32 | f.set_len(0); // Truncate the file
| ^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
Explicitly using a match
allows an error to propagate, but at the cost of some visible boilerplate (reminiscent of
Go):
#![allow(unused)] fn main() { pub fn find_user(username: &str) -> Result<UserId, std::io::Error> { let f = match std::fs::File::open("/etc/passwd") { Ok(f) => f, Err(e) => return Err(e), }; // ... } }
The key ingredient for reducing boilerplate is Rust's question mark operator
?
. This piece of
syntactic sugar takes care of matching the Err
arm and the return Err(...)
expression in a single character:
#![allow(unused)] fn main() { pub fn find_user(username: &str) -> Result<UserId, std::io::Error> { let f = std::fs::File::open("/etc/passwd")?; // ... } }
Newcomers to Rust sometimes find this disconcerting: the question mark can be hard to spot on first glance, leading to disquiet as to how the code can possibly work. However, even with a single character, the type system is still at work, ensuring that all of the possibilities expressed in the relevant types (Item 1) are covered – leaving the programmer to focus on the mainline code path without distractions.
What's more, there's generally no cost to these apparent method invocations: they are all generic functions marked
as #[inline]
, so
the generated code will typically compile to machine code that's identical to the manual version.
These two factors taken together mean that you should prefer Option
and Result
transforms to explicit match
expressions.
In the previous example, the error types lined up: both the inner and outer methods expressed errors as
std::io::Error
. That's often not the case; one function may
accumulate errors from a variety of different sub-libraries, each of which uses different error types.
Error mapping in general is discussed in Item 4; for now, just be aware that a manual mapping:
#![allow(unused)] fn main() { pub fn find_user(username: &str) -> Result<UserId, String> { let f = match std::fs::File::open("/etc/passwd") { Ok(f) => f, Err(e) => { return Err(format!("Failed to open password file: {:?}", e)) } }; // ... } }
can be more succinctly and idiomatically expressed with the
.map_err()
transformation:
#![allow(unused)] fn main() { pub fn find_user(username: &str) -> Result<UserId, String> { let f = std::fs::File::open("/etc/passwd") .map_err(|e| format!("Failed to open password file: {:?}", e))?; // ... } }
Better still, even this may not be necessary – if the outer error type can be created from the inner error type
via an implementation of the From
standard trait (Item 5), then the compiler will automatically perform the
conversion without the need for a call to .map_err()
.
These kinds of transformations generalize more widely. The question mark operator is a big hammer; use transformation
methods on Option
and Result
types to manoeuvre them into a position where they can be a nail.
The standard library provides a wide variety of these transformation methods to make this possible, as shown in the
following map. In line with Item 18, methods that can panic!
are highlighted in red.
(The online version of this diagram is clickable: each box links to the relevant documentation.)
One common situation which isn't covered by the diagram is dealing with references. For example, consider a structure that optionally holds some data.
struct InputData {
payload: Option<Vec<u8>>,
}
A method on this struct
which tries to pass the payload to an encryption function with signature (&[u8]) -> Vec<u8>
fails if there's a naive attempt to take a reference:
impl InputData {
pub fn encrypted(&self) -> Vec<u8> {
encrypt(&self.payload.unwrap_or(vec![]))
}
}
error[E0507]: cannot move out of `self.payload` which is behind a shared reference
--> transform/src/main.rs:62:22
|
62 | encrypt(&self.payload.unwrap_or(vec![]))
| ^^^^^^^^^^^^ move occurs because `self.payload` has type `Option<Vec<u8>>`, which does not implement the `Copy` trait
|
help: consider borrowing the `Option`'s content
|
62 | encrypt(&self.payload.as_ref().unwrap_or(vec![]))
| +++++++++
The error message describes exactly what's needed to make the code work, the
as_ref()
method1 on Option
. This method converts a reference-to-an-Option
to be an Option
-of-a-reference:
pub fn encrypted(&self) -> Vec<u8> {
encrypt(self.payload.as_ref().unwrap_or(&vec![]))
}
To sum up:
- Get used to the transformations of
Option
andResult
, and preferResult
toOption
.- Use
.as_ref()
as needed when transformations involve references.
- Use
- Use them in preference to explicit
match
operations. - In particular, use them to transform result types into a form where the
?
operator applies.
1: Note that this method is separate from the AsRef
trait, even though the method name is the
same.