Rust is a systems programming language that focuses on safety and performance, and has been voted the “most loved language” on Stack Overflow’s annual survey for six years running! One of the reasons Rust is such a joy to program in is that, despite its focus on performance, it has a lot of well-thought-out conveniences that are frequently associated with higher-level languages.
One of these conveniences is using enums, specifically the Option
and Result
types. So, in this post we’ll cover the following:
The Option
type
Rust’s version of a nullable type is the Option<T>
type. It’s an enumerated type (also known as algebraic data types in some other languages) where every instance is either:
None
- or
Some(value)
This is where value
can be any value of type T
. For example, Vec<T>
is Rust’s type that represents a vector (or variable-sized array). It has a pop()
method that returns an Option<T>
, which will be None
if the vector is empty or Some(value)
containing the last value of the vector.
One of the benefits of an API that returns an Option
is that to get the value inside, callers are forced to check if the value is None
or not. This avoids problems in other languages that don’t have nullable types.
For example, in C++, std::find()
returns an iterator, but you must remember to check it to make sure it isn’t the container’s end()
—if you forget this check and try to get the item out of the container, you get undefined behavior.
The downside is that this tends to make code irritatingly verbose. But, Rust has a lot of tricks up its sleeve to help!
Using expect
and unwrap
If you’re sure that an Option
has a real value inside, then expect()
and unwrap()
are for you! They return the value inside, but if the variable is actually None
, your program exits. (This is known as panicking, and there are cases when it is recoverable, but for simplicity, we’ll gloss over that here.)
The only difference is that expect()
lets you specify a custom message that prints out to the console as the program exits.
There’s also an unwrap_or()
, which lets you specify a default if the value is None
, so Some(5).unwrap_or(7)
is 5
and None.unwrap_or(7)
is 7
.
If you want, you can check whether the Option<T>
has a value before calling unwrap()
like this:
// t is an Option<T> if t.is_some() { let real_value = t.unwrap(); }
But, there are more concise ways to do this (for instance, using if let
, which we’ll cover later).
Using match
The most basic way to see whether an Option
has a value or not is to use pattern matching with a match
expression. This works on any enumerated type, and looks like this:
// t is an Option<T> match t { None => println!("No value here!"), // one match arm Some(x) => println!("Got value {}", x) // the other match arm };
One thing to note is that the Rust compiler enforces that a match
expression must be exhaustive; that is, every possible value must be covered by a match arm. So, the following code won’t compile:
// t is an Option<T> match t { Some(x) => println!("Got value {}", x) };
And we get an error:
error[E0004]: non-exhaustive patterns: `None` not covered
This is actually very helpful to avoid times when you think you’re covering all the cases but aren’t! If you explicitly want to ignore all other cases, you can use the _
match
expression:
// t is an Option<T> match t { Some(x) => println!("Got value {}", x), // the other match arm _ => println!("OK not handling this case."); };
Using if let
It’s pretty common to want to do something only if an Option
has a real value, and if let
is a concise way to combine doing that with getting the underlying value.
For instance, the following code will print "Got <value>"
if t
has a value, and do nothing if t
is None
:
// t is an Option<T> if let Some(i) = t { println!("Got {}", i); }
if let
actually works with any enumerated type!
Using map
There are also a bunch of ways to do things to an Option<T>
without checking whether it has a value or not. As an example, you can use map()
to transform the real value if it has one, and otherwise leave it as None
.
So, for example, Some(10).map(|i| i + 1)
is Some(11)
and None.map(|i| i + 1)
is still None
.
Using into_iter
with Option
If you have a Vec<Option<T>>
, you can transform this into an Option<Vec<T>>
, which will be None
if any of the entries in the original vector were None
.
This makes sense if you think about receiving results from many operations and you want the overall result to fail if any of the individual operations failed.
So, for example vec![Some(10), Some(20)].into_iter().collect()
is Some([10, 20])
while vec![Some(10), Some(20), None].into_iter().collect()
is None
.
The Result
type
Rust’s Result<T, E>
type is a convenient way of returning either a value or an error. Like the Option
type, it’s an enumerated type with two possible variants:
Ok(T)
, meaning the operation succeeded with valueT
Err(E)
, meaning the operation failed with an errorE
It’s very convenient to know that if a function returns an error, it will be this type, and there are a bunch of helpful ways to use them!
Using ok_or
Since Option
and Result
are so similar, there’s an easy way to go between the two. Option
has the ok_or()
method: Some(10).ok_or("uh-oh")
is Ok(10)
and None.ok_or("uh-oh")
is Err("uh-oh")
.
Then, Result
has the ok()
method: Ok(10).ok()
is Some(10)
and Err("uh-oh").ok()
is None
.
There’s also an err()
method on Result
that does the opposite: errors get mapped to Some
and success values get mapped to None
.
Using expect
, unwrap
, match
, and if let
Just like with Option
, if you’re sure a Result
is a success (and you don’t mind exiting if you’re wrong!), expect()
and unwrap()
work exactly the same way as they do for Option
.
And, since Result
is an enumerated type, match
and if let
work in the same way, too!
Using the ?
operator
Ok, this is where things get really cool. Let’s say you’re writing a function that returns a Result
because it could fail, and you’re calling another function that returns a Result
because it could fail.
Many times if the other function returns an error, you want to return that error straight out of the function. So, your code would look like the following:
let inner_result = other_function(); if let Err(some_error) = inner_result { return inner_result; } let real_result = inner_result().unwrap(); // now real_result has the actual value we care about, we can continue on...
But, this is kind of a pain to write over and over. Instead, you can write this code:
let real_result = other_function()?;
That’s right: the single ?
operator does all of that! What’s even better is that you can chain calls together, like so:
let real_result = this_might_fail()?.also_might_fail()?.this_one_might_fail_too()?;
Another common technique is to use something like map_err()
to transform the error into something that makes more sense for the outer function to return, then use the ?
operator.
Using must_use
The Rust compiler is notoriously helpful, and one of the ways it helps is by warning you about mistakes you might be making.
The Result
type is tagged with the must_use
attribute, which means that if a function returns a Result
, the caller must not ignore the value, or the compiler will issue a warning.
This is mostly a problem with functions that don’t have a real value to return, like I/O functions; many of them return types like Result<(), Err>
(()
is known as the unit type), and in this case, it’s easy to forget to check the error since there’s no success value to get.
But, the compiler is there to help you remember!
Using into_iter
with Result
Similar to Option
, if you have a Vec<Result<T, E>>
you can use into_iter()
and collect()
to transform this into a Result<Vec<T>, E>
, which will either contain all the success values or the first error encountered.
So, for example, the following is Ok([10, 20])
:
vec![Ok(10), Ok(20)].into_iter().collect()
Then, this is Err("bad")
:
vec![Ok(10), Err("bad"), Ok(20), Err("also bad")].into_iter().collect()
If you want to gather all the errors instead of just the first one, it’s a little trickier, but you can use the handy partition()
method to split the successes from the errors:
let v: Vec<Result<_, _>> = some_other_function(); let (successes, errors): (Vec<_>, Vec<_>) = v.into_iter().partition(Result::is_ok); if !errors.is_empty() { return Err(errors.into_iter().map(Result::unwrap_err).collect()); } else { return Ok(successes.into_iter().map(Result::unwrap).collect()); }
Conclusion
The ideas behind Option
and Result
are not new to Rust. What stands out for me is how easy the language makes it to do the right thing by checking errors, especially with the ?
operator.
The post Understanding Rust <code>Option</code> and <code>Results</code> enums appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/Fu7cKJ3
via Read more