Shiklo Tools
PDF40
Image20
Calculators & Converters25
Text & Writing13
Developer26
Video14
Audio14
Document14
Data & Spreadsheet12
Web & SEO9
OCR & Scanning5
Archive & File12
Security & Privacy23
Health & Medical29
QR & Barcode5

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());
}
Output
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),
        }
    }
}
Output
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);
}
Output
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);
}
Output
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

  1. Write fn first_even(numbers: &[i32]) -> Option<i32> and test it with a list that has an even number and one that does not.
  2. Use if let to print the position of the letter 'z' in a word, only if it is there.
  3. Use unwrap_or to give a default name of "Guest" when a name is None.