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 else part of the let else statement must diverge from the happy path. In the above example, the else part is a println! statement and a return statement. The return statement 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}")
    }
}