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
0as 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_agevariable.
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() }