Back to the basics - optional values
We'll continue with the code from the previous chapter. If you've skipped ahead, you may want to circle back and ensure you cover that chapter, before continuing.
In our current code, we're returning the default value 0 when the user makes an input error. In essence, we are using
the perfectly valid, although unlikely, age of 0 to indicate an error. Rather than relying on default values, Rust
allows us to return an optional value from read_number()
. This means we can return a valid u8
number, or nothing.
This is achieved with the Option
type.
The signature of Option
is: Option< [embedded type] >
Where [embedded type]
is the data type we want our Option
to wrap. In our case we would use Option<u8>
to wrap the u8
in an Option
.
Let's amend our code to reflect this.
use std::str::FromStr; fn main() { println!("How old are you?"); let optional_age = read_number(); if optional_age.is_some() { println!("You are {} years old.", optional_age.unwrap()); } else { println!("You've entered an invalid age"); } } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> Option<u8> { let input = read_string(); u8::from_str(&input).ok() }
The return value of read_number()
has been changed to return a Option<u8>
. The unwrap_or(0)
has been replaced
with ok()
.
Valid return values from a Option<u8>
are: Some(u8)
or None
. The ok()
method will attempt to take a valid u8
from the from_str()
method. If one is found, it will return it as Some(number)
otherwise it will return None
.
Because the u8
is now wrapped inside the Option
we can test if there was a return value. Our if
statement now
checks for age.is_some()
. Which is true
when there is a valid u8
returned in the Option
.
Exercise
Run the example and see what happens when you enter a
0
as input.
Much nicer, isn't it?!
There is however one nasty piece of code in our program: optional_age.unwrap()
.
Because we are first testing that there is a valid value in the Option
, the unwrap
will always succeed. So from
that perspective it is a completely valid piece of code.
However, in the previous chapter I've told you to be scarce in the use of unwrap
, so there must surely be a way to
handle the optional return without resorting to unwrap
or expect
.
In Rust, we can use the if let
statement for this:
use std::str::FromStr; fn main() { println!("How old are you?"); let optional_age = read_number(); if let Some(age) = optional_age { println!("You are {} years old.", age); } else { println!("You've entered an invalid age"); } } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> Option<u8> { let input = read_string(); u8::from_str(&input).ok() }
The if let Some(age) = optional_age
does two things in one step:
- It attempts to assign the value inside
optional_age
, which is anOption<u8>
, toage
. - It checks if this was successful.
For the most part it behaves in the same way as a regular if
statement. But in the positive case, there is now
a new local variable age
that you can use within that block of code. age
is of type u8
.
There you have it! A piece of code that handles input errors elegantly, without the need for unwrap
or expect
.
Exercise
Rewrite the above example, without the intermediate
optional_age
variable.
Matching results
Alternative to the if let
statement, Rust offers a match
statement. match
allows you to run a piece of code for
all possible return values. In the case of an Option
, this is either Some(value)
or None
. We'll rewrite the above
code using a match
block.
use std::str::FromStr; fn main() { println!("How old are you?"); let optional_age = read_number(); match optional_age { None => { println!("You've entered an invalid age"); } Some(age) => { println!("You are {} years old.", age); } } } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> Option<u8> { let input = read_string(); u8::from_str(&input).ok() }
This code behaves identical to the previous example. The match
statements checks the result of optional_age
and
runs either the None
block, or the Some(age)
block. The =>
is used as a separator, between the return value and
the block that is run.
It is up to the developer - you - to decide if you prefer the if let
construct, or a match
block.
If you are executing only a single command in a match
block, you can ditch the {
pairs, and write the code more
concisely. Here's the same code written in fewer lines:
use std::str::FromStr; fn main() { println!("How old are you?"); match read_number() { None => println!("You've entered an invalid age"), Some(age) => println!("You are {} years old.", age), } } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> Option<u8> { let input = read_string(); u8::from_str(&input).ok() }