Rust Option
Many values in life might simply not be there. A phone book might not have the name you are looking for. A list might be empty, so there is no first item. A user might leave a form field blank. Many languages represent "nothing" with a special value called null, and its inventor later called it his "billion dollar mistake", because programs so often forget to check for it and crash.
Rust has no null. Instead, a value that might be missing has a different type: Option. It is an enum with two variants:
Some(value): there is a value, and here it is.None: there is no value.
Because Option<i32> is a different type from i32, you cannot use it as a number by accident. Rust makes you look inside first, and that is exactly what prevents the crashes.
Getting an Option
Lots of standard methods give you an Option. first on a list, find on a string, get on a vector, max on numbers: all of them might have nothing to give.
fn main() {
let numbers = vec![4, 8, 15];
let empty: Vec<i32> = Vec::new();
println!("{:?}", numbers.first());
println!("{:?}", empty.first());
}Some(4) None
Looking inside with match
The full way to handle an Option is a match with an arm for each variant. Rust insists that you handle None too:
fn find_phone(name: &str) -> Option<&str> {
match name {
"Asha" => Some("98765 43210"),
"Ravi" => Some("91234 56789"),
_ => None,
}
}
fn main() {
for person in ["Ravi", "Zara"] {
match find_phone(person) {
Some(number) => println!("{}: {}", person, number),
None => println!("{}: not in the phone book", person),
}
}
}Ravi: 91234 56789 Zara: not in the phone book
Shortcuts: if let and unwrap_or
When you only care about the Some case, if let is shorter than a full match. Read it as "if this pattern fits, then...":
fn main() {
let words = vec!["sun", "planet", "star"];
if let Some(longest) = words.iter().max_by_key(|w| w.len()) {
println!("The longest word is {}", longest);
}
let maybe_age: Option<u32> = None;
let age = maybe_age.unwrap_or(18);
println!("Using age {}", age);
}The longest word is planet Using age 18
unwrap_or(default) gives you the value inside, or the default when there is none. It is perfect for settings that have a sensible fallback.
Doing maths that might fail
Here is a function that returns an Option itself. Dividing by zero has no answer, so the function says so honestly with None:
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
println!("{:?}", divide(9.0, 2.0));
println!("{:?}", divide(1.0, 0.0));
let half = divide(10.0, 2.0).map(|x| x * 100.0);
println!("{:?}", half);
}Some(4.5) None Some(500.0)
map changes the value inside a Some and leaves None alone, so you can keep working without unpacking.
A word about unwrap
unwrap() takes the value out of a Some, and stops the program if it finds None. It is fine in quick experiments, but in a finished program prefer match, if let or unwrap_or, so a missing value is handled instead of becoming a crash.
Practice exercises
- Write
fn first_even(numbers: &[i32]) -> Option<i32>and test it with a list that has an even number and one that does not. - Use
if letto print the position of the letter 'z' in a word, only if it is there. - Use
unwrap_orto give a default name of "Guest" when a name isNone.