Optionals
In Rust, there is no concept of "null" or "nil". Which is a good thing! In order to use variables that may not have a
value, you wrap it in an Option. Clearly you need to test if an Option has a value, before you can use it, which is
what we will explore in this chapter.
struct Person { name: String, age: u8, address: Option<String>, } impl Person { fn new(name: String, age: u8) -> Self { Person { name, age, address: None, } } } fn main() { let person = Person::new("Marcel".to_string(), 40); let Person { name: my_name, age, address: _address, } = person; println!("{my_name} is {age} years old"); }
This example behaves exactly the same as the previous one, but has a placeholder for an optional address, that is
initialized without a value by the new function.
Let's expand the example and assign a value to address:
struct Person { name: String, age: u8, address: Option<String>, } impl Person { fn new(name: String, age: u8) -> Self { Person { name, age, address: None, } } } fn main() { let mut person = Person::new("Marcel".to_string(), 40); person.address = Some("Developer Ave 10".to_string()); let Person { name: my_name, age, address, } = person; if let Some(my_address) = address { println!("{my_name} is {age} years old and lives at {my_address}"); } else { println!( "{my_name} is {age} years old. We have no address on file" ); } }
You can see that we use the None and Some shorthand wrappers to assign a value, or no value, to an Option. Before
an optional value can be used, it has to be tested to see if it contains a value. The if let Some(...) statement is a
typical way of doing this.
In the above example the if let Some(my_address) = address statement, creates a new variable my_address in
case address has a value. The my_address variable is scoped to the if block.
If you know for fact that an Option has a value you can forcefully "unwrap" the Option. You use either
the unwrap() or expect() method. Both methods will panic in case the value is None. expect() allows you to
provide a meaningful message while panicking. That is why you will unlikely use unwrap() anywhere outside of
development. It makes debugging unnecessary hard.
Same example with `expect()
struct Person { name: String, age: u8, address: Option<String>, } impl Person { fn new(name: String, age: u8) -> Self { Person { name, age, address: None, } } } fn main() { let mut person = Person::new("Marcel".to_string(), 40); person.address = Some("Developer Ave 10".to_string()); if person.address.is_some() { println!( "{} is {} years old and lives at {}", person.name, person.age, person.address.expect("address has no value") ); } else { println!( "{} is {} years old. We have no address on file", person.name, person.age ); } }
The use of if and is_some() is not the idiomatic way of doing this. The if let Some(...) statement is the preferred
way of doing this. The above example is just to show you that you can use is_some() to check if an Option has a
value.
See what happens when you remove the address assignment and the
is_some()check:
struct Person { name: String, age: u8, address: Option<String>, } impl Person { fn new(name: String, age: u8) -> Self { Person { name, age, address: None, } } } fn main() { let person = Person::new("Marcel".to_string(), 40); println!( "{} is {} years old and lives at {}", person.name, person.age, person.address.expect("address has no value") ); }
Panicking is not undefined behaviour, but it is not a good practice to panic in production code. It is better to handle
the situation gracefully. In the above example, we could have used the if let Some(...) statement to handle the
situation.
Since Rust 1.65 you can also use the let else statement to handle the situation. Here's the above example using the
let else statement:
struct Person { name: String, age: u8, address: Option<String>, } impl Person { fn new(name: String, age: u8) -> Self { Person { name, age, address: None, } } } fn main() { let person = Person::new("Marcel".to_string(), 40); let Some(address) = person.address else { println!( "{} is {} years old. We have no address on file", person.name, person.age ); return; }; println!( "{} is {} years old and lives at {}", person.name, person.age, address ); }
The let else can be a useful pattern to avoid deep nesting of if let Some statements. It uses the quick return
pattern.
Note that the
elsepart of thelet elsestatement must diverge from the happy path. In the above example, theelsepart is aprintln!statement and areturnstatement. Thereturnstatement is necessary to stop the execution of the function. If you don't return, you will get a compiler error.
You will typically use return, break, continue or panic! to diverge from the happy path.
Functions in Rust that deal with optional data will either return an Option or panic!. If a function that could
point to a non-existent item does not return an Option, you can expect it to panic when it can't find the item
you're requesting. Check the documentation to understand the different behaviour.
Here's such an example:
Panics:
fn main() { let mut names = vec!["Tom", "Dick", "Harry"]; let last_name_in_list = names.remove(3); println!("{last_name_in_list}") }
If the intent was to remove and display the last item from the list, the pop() function would have been a better fit.
fn main() { let mut names = vec!["Tom", "Dick", "Harry"]; if let Some(last_name_in_list) = names.pop() { println!("{last_name_in_list}") } }
Similar to if let, Rust supports a while let variant to loop until a None value is found. Let's adapt the above
example to print all names in the list (in reverse order).
fn main() { let mut names = vec!["Tom", "Dick", "Harry"]; while let Some(last_name_in_list) = names.pop() { println!("{last_name_in_list}") } }