Why this matters
Many operations may have no result: looking up a missing key, taking the first element of an empty list, finding a character that is not there. Most languages represent "no value" with null, and forgetting to check for null is so common that its inventor, Tony Hoare, called it his "billion-dollar mistake". Rust has no null. Instead, a possibly-missing value has a different type, Option<T>, and the compiler will not let you use it as a T until you have handled the missing case.
The Option enum
Option is an ordinary enum from the standard library:
enum Option<T> {
None,
Some(T),
}
<T> makes it generic (module 10): Option<i32>, Option<String> and Option<&str> are all Options. It is so common that Some and None can be used without the Option:: prefix.
fn first_even(values: &[i32]) -> Option<i32> {
for &v in values {
if v % 2 == 0 {
return Some(v);
}
}
None
}
fn main() {
let a = first_even(&[3, 7, 8, 10]);
let b = first_even(&[1, 5]);
println!("{:?} {:?}", a, b);
match a {
Some(n) => println!("got {n}"),
None => println!("nothing"),
}
}
Some(8) None
got 8
The key point: an Option<i32> is not an i32. You cannot add 1 to it:
fn main() {
let maybe: Option<i32> = Some(5);
let n = maybe + 1;
}
error[E0369]: cannot add `{integer}` to `Option<i32>`
--> main.rs:3:19
|
3 | let n = maybe + 1;
| ----- ^ - {integer}
| |
| Option<i32>
|
note: the foreign item type `Option<i32>` doesn't implement `Add<{integer}>`
--> /usr/src/rustc-1.85.1/library/core/src/option.rs:572:1
|
= note: not implement `Add<{integer}>`
Every place that wants the number must first decide what to do if it is absent. The compiler forces the null check that other languages leave to discipline.
Getting the value out
| method | if Some(v) | if None |
|---|---|---|
match / if let | your code | your code |
unwrap() | v | panic |
expect("msg") | v | panic with your message |
unwrap_or(default) | v | default |
unwrap_or_else(|| f()) | v | result of f(), computed only when needed |
unwrap_or_default() | v | the type's default (0, empty string...) |
is_some() / is_none() | true / false | false / true |
fn main() {
let words = ["alpha", "beta"];
let first = words.first();
let tenth = words.get(10);
println!("{:?} {:?}", first, tenth);
println!("{}", tenth.unwrap_or(&"none"));
println!("{}", first.expect("list is never empty here"));
let n: Option<u32> = None;
println!("{}", n.unwrap_or_default());
}
Some("alpha") None
none
alpha
0
words.get(i) is the non-panicking version of words[i]: it returns Option<&T>. Many standard functions come in such pairs.
unwrap and expect are fine when absence would be a bug in your own program (an invariant you know holds), and in quick experiments. Prefer expect with a message saying why it cannot fail; when it does, the message tells you which assumption broke.
Transforming without unwrapping: combinators
Often you want to transform the value if present and keep None otherwise. Option has methods for that, which take small inline functions called closures, written |x| expression (module 11):
fn main() {
let name: Option<&str> = Some("ada");
let upper = name.map(|s| s.to_uppercase());
println!("{:?}", upper);
let len = name.map(|s| s.len()).unwrap_or(0);
println!("{len}");
let text = "17";
let parsed: Option<i32> = text.parse().ok();
let halved = parsed.and_then(|n| if n % 2 == 0 { Some(n / 2) } else { None });
println!("{:?} {:?}", parsed, halved);
let big = parsed.filter(|&n| n > 10);
println!("{:?}", big);
}
Some("ADA")
3
Some(17) None
Some(17)
map(f):Some(v)becomesSome(f(v)),NonestaysNone.and_then(f): likemap, butfitself returns anOption, so steps that may fail can be chained.filter(pred): keeps the value only if the predicate holds.ok()converts aResult(next lesson) into anOption, discarding the error.ok_or(err)goes the other way, turningNoneinto an error value.
These chains read as a pipeline: "take the first word, parse it, double it", with absence handled once at the end.
The ? operator on Option
Inside a function that returns Option, expr? means "if this is None, return None right now; otherwise give me the value":
fn middle_initial(full_name: &str) -> Option<char> {
let mut parts = full_name.split_whitespace();
let _first = parts.next()?;
let middle = parts.next()?;
let _last = parts.next()?;
middle.chars().next()
}
fn main() {
println!("{:?}", middle_initial("Grace Brewster Hopper"));
println!("{:?}", middle_initial("Ada Lovelace"));
}
Some('B')
None
Each ? is an early exit. Lesson 3 covers ? in depth for Result, where it is used most.
Borrowing the inside: as_ref and as_mut
Methods like map and unwrap take the Option by value, moving its contents. For an Option<String> you want to keep, borrow first:
fn main() {
let nickname: Option<String> = Some(String::from("Ada"));
let len = nickname.as_ref().map(|s| s.len());
println!("{:?} still have {:?}", len, nickname);
if let Some(n) = &nickname {
println!("hello {n}");
}
}
Some(3) still have Some("Ada")
hello Ada
as_ref() turns &Option<String> into Option<&String>. Matching on &nickname binds a reference, as in the last lesson.
What Option costs
Nothing, often. For references and Box (pointers that can never be null), Rust represents None as the null address, so Option<&T> is a plain pointer:
use std::mem::size_of;
fn main() {
println!("&i32 {} / Option<&i32> {}", size_of::<&i32>(), size_of::<Option<&i32>>());
println!("u64 {} / Option<u64> {}", size_of::<u64>(), size_of::<Option<u64>>());
println!("String {} / Option<String> {}", size_of::<String>(), size_of::<Option<String>>());
}
&i32 8 / Option<&i32> 8
u64 8 / Option<u64> 16
String 24 / Option<String> 24
You get exactly what a C programmer would write (a nullable pointer), except that the compiler checks every use.
Common mistakes
- Unwrapping everywhere. It moves the crash from "null pointer exception" to "called
Option::unwrap()on aNonevalue". HandleNoneor useexpectwith a reason. - Returning sentinel values like
-1or""for "not found". ReturnOptionand let the type say it. - Moving out of an Option you still need. Use
as_ref(),&optorif let Some(x) = &opt. - Nesting
matchinmatchfor chains of optional steps. Useand_then,mapor?.
Recap
Option<T>isSome(T)orNone; there is no null, and anOption<T>cannot be used as aT.- Get the value with
match,if let,let else,unwrap_or,unwrap_or_else, or (when absence is a bug)expect. - Transform with
map,and_then,filter; convert withok()andok_or(). ?returns early withNoneinside a function returningOption.Option<&T>andOption<Box<T>>are pointer-sized: null isNone.
Going further: look at the long method list on Option in the standard library docs; try zip, take, replace and get_or_insert.