Lesson 22 of 94
Option: Replacing null
Tony Hoare, who invented the null reference, called it his "billion-dollar mistake": the type system does not tell you which references might be null, and you find out with a crash at runtime.
Rust has no null. A value that might be absent has the type Option<T>, an ordinary enum from the standard library:
enum Option<T> {
None, // no value
Some(T), // a value of type T
}The compiler will not let you use an Option<T> as if it were a T. You must handle None first. This is what makes null-dereference errors impossible.
Option<T> uses generics: T stands for any type, so there is Option<i32>, Option<String>, and so on. You only need to use generic types here; Generics shows how to write your own.
| Concept | Rust |
|---|---|
| A missing value | let name: Option<String> = None; |
| Checking for a value | if let Some(name) = name { ... } |
| Transforming with a default | x.map(f).unwrap_or(d) |
| Forcing the value out | option.unwrap() (panics if None) |
| Turning absence into an error | option.ok_or(X)? |
| Which values can be absent | Only an Option<T> can be None |
1. Creating Some and None
Some and None are so common that they can be used without the Option:: prefix.
fn main() {
let middle_name: Option<&str> = Some("Brewster");
let no_middle_name: Option<&str> = None;
let count = Some(3); // Option<i32>, inferred
println!("{:?} {:?} {:?}", middle_name, no_middle_name, count);
}Some("Brewster") None Some(3)None alone does not say what type is missing, so the compiler needs an annotation, or context, to know whether it is an Option<&str> or an Option<i32>.
2. An Option<i32> is not an i32
This is the whole point. You cannot accidentally treat a maybe-missing value as present.
fn main() {
let discount: Option<i32> = Some(10);
let doubled = discount * 2;
println!("{doubled}");
}error[E0369]: cannot multiply `Option<i32>` by `{integer}`
--> src/main.rs:3:28
|
3 | let doubled = discount * 2;
| -------- ^ - {integer}
| |
| Option<i32>
|
note: `Option<i32>` does not implement `Mul<{integer}>`
--> /rustc/<version>/library/core/src/option.rs:600:0
|
= note: `Option<i32>` is defined in another crate3. Handling both cases with match
fn describe(discount: Option<i32>) -> String {
match discount {
Some(percent) => format!("{percent}% off"),
None => String::from("full price"),
}
}
fn main() {
println!("{}", describe(Some(15)));
println!("{}", describe(None));
}15% off
full price4. A function that might not find anything
Return Option from any function that can come back empty-handed. The signature documents it, and every caller is forced to deal with it.
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn find_user(users: &[User], id: u32) -> Option<&User> {
for u in users {
if u.id == id {
return Some(u);
}
}
None
}
fn main() {
let users = vec![User { id: 1, name: String::from("Ana") }, User { id: 2, name: String::from("Ben") }];
match find_user(&users, 2) {
Some(user) => println!("found {}", user.name),
None => println!("no such user"),
}
println!("{:?}", find_user(&users, 99));
}found Ben
None5. The standard library returns Option everywhere
Any operation that might not produce a value returns an Option.
fn main() {
let scores = vec![72, 95, 88];
let empty: Vec<i32> = Vec::new();
println!("get(1): {:?}", scores.get(1));
println!("get(10): {:?}", scores.get(10));
println!("first: {:?}", scores.first());
println!("last of empty: {:?}", empty.last());
println!("max: {:?}", scores.iter().max());
println!("find '@': {:?}", "user@example.com".find('@'));
println!("char at 20: {:?}", "short".chars().nth(20));
println!("parse ok: {:?}", "42".parse::<i32>().ok());
println!("parse bad: {:?}", "4x2".parse::<i32>().ok());
}get(1): Some(95)
get(10): None
first: Some(72)
last of empty: None
max: Some(95)
find '@': Some(4)
char at 20: None
parse ok: Some(42)
parse bad: None.ok() converts a Result into an Option, discarding the error details.
6. if let when you only care about Some
fn main() {
let cached_token: Option<&str> = Some("eyJhbGciOi");
if let Some(token) = cached_token {
println!("using cached token {token}...");
}
let nickname: Option<&str> = None;
if let Some(n) = nickname {
println!("hi {n}");
} else {
println!("hi there");
}
}using cached token eyJhbGciOi...
hi there7. let ... else for early returns
let Some(x) = value else { ... }; binds x when the value is Some. Otherwise it runs the else block, which must leave the function (with return, break, continue, or a panic). This keeps the happy path unindented.
fn shipping_label(name: Option<&str>, address: Option<&str>) -> String {
let Some(name) = name else {
return String::from("ERROR: missing name");
};
let Some(address) = address else {
return format!("ERROR: missing address for {name}");
};
format!("{name}\n{address}")
}
fn main() {
println!("{}", shipping_label(Some("Ada Lovelace"), Some("12 St James's Sq, London")));
println!("{}", shipping_label(Some("Grace"), None));
println!("{}", shipping_label(None, None));
}Ada Lovelace
12 St James's Sq, London
ERROR: missing address for Grace
ERROR: missing name8. unwrap panics on None
unwrap() returns the value inside Some, and panics on None. Use it only in examples, tests, or when you have proved the value is present.
fn main() {
let items: Vec<&str> = Vec::new();
let first = items.first().unwrap();
println!("{first}");
}thread 'main' panicked at src/main.rs:3:31:
called `Option::unwrap()` on a `None` value9. expect documents why it cannot fail
expect("...") is unwrap with a message. When you are sure a value is present, write down why in the message, so if you were wrong the panic explains itself.
fn main() {
let config_lines = vec!["port=8080"];
let port_line = config_lines
.iter()
.find(|l| l.starts_with("host="))
.expect("config must define a host");
println!("{port_line}");
}thread 'main' panicked at src/main.rs:6:10:
config must define a host10. Default values: unwrap_or, unwrap_or_default, unwrap_or_else
fn expensive_default() -> u32 {
println!(" (computing default...)");
8080
}
fn main() {
let configured: Option<u32> = None;
let present: Option<u32> = Some(3000);
println!("{}", configured.unwrap_or(80));
println!("{}", configured.unwrap_or_default()); // 0 for numbers
println!("{}", configured.unwrap_or_else(expensive_default));
println!("{}", present.unwrap_or_else(expensive_default)); // not called
}80
0
(computing default...)
8080
3000unwrap_or_else takes a function and only calls it when needed. Use it when the default is expensive to compute.
11. map transforms the value inside
map applies a function to the value if there is one, and leaves None alone. The function here is a closure, Rust's lambda: |x| x * 2. Closures covers them fully.
fn main() {
let price_cents: Option<u64> = Some(1_999);
let missing: Option<u64> = None;
let formatted = price_cents.map(|c| format!("${}.{:02}", c / 100, c % 100));
println!("{:?}", formatted);
println!("{:?}", missing.map(|c| c * 2));
let name: Option<&str> = Some("ferris");
let length = name.map(|n| n.len());
println!("{:?}", length);
}Some("$19.99")
None
Some(6)12. and_then chains steps that can each fail
When the function you want to apply itself returns an Option, map would give you an Option<Option<T>>. and_then flattens it.
fn parse_port(s: &str) -> Option<u16> {
s.parse().ok()
}
fn non_privileged(port: u16) -> Option<u16> {
if port >= 1024 { Some(port) } else { None }
}
fn main() {
for input in ["8080", "80", "http"] {
let port = Some(input).and_then(parse_port).and_then(non_privileged);
println!("{input:>5} -> {:?}", port);
}
} 8080 -> Some(8080)
80 -> None
http -> None13. filter keeps the value only if it passes a test
fn main() {
let age: Option<u32> = Some(15);
let adult = age.filter(|&a| a >= 18);
println!("{:?} -> adult: {:?}", age, adult);
let username: Option<&str> = Some(" ");
let non_blank = username.filter(|u| !u.trim().is_empty());
println!("{:?}", non_blank);
}Some(15) -> adult: None
None14. ? returns early on None
Inside a function that returns Option, writing ? after an Option means: "if this is None, return None from the whole function right now; otherwise give me the value." It removes most match boilerplate.
fn domain_of(email: &str) -> Option<&str> {
let at = email.find('@')?; // None if there is no '@'
let domain = &email[at + 1..];
let dot = domain.find('.')?; // None if the domain has no '.'
if dot == 0 {
return None;
}
Some(domain)
}
fn main() {
println!("{:?}", domain_of("ana@example.com"));
println!("{:?}", domain_of("no-at-sign"));
println!("{:?}", domain_of("ben@localhost"));
println!("{:?}", domain_of("cy@.com"));
}Some("example.com")
None
None
None? also works with Result, where it is even more important; see Result & the ? Operator.
15. Checking without extracting
fn main() {
let session: Option<u64> = Some(42);
let guest: Option<u64> = None;
println!("{} {}", session.is_some(), session.is_none());
println!("{} {}", guest.is_some(), guest.is_none());
println!("{}", session.is_some_and(|id| id > 40));
println!("{}", guest.is_none_or(|id| id > 40));
}true false
false true
true
true16. Borrowing the inside: as_ref and as_deref
Calling map on an Option<String> would move the String out. To just look inside, turn &Option<String> into Option<&String> with as_ref(), or straight into Option<&str> with as_deref().
struct Profile {
display_name: Option<String>,
}
fn main() {
let p = Profile { display_name: Some(String::from("Ferris the Crab")) };
let length = p.display_name.as_ref().map(|n| n.len());
let as_str: Option<&str> = p.display_name.as_deref();
let shown = p.display_name.as_deref().unwrap_or("anonymous");
println!("{:?} {:?} {shown}", length, as_str);
println!("still owned by the profile: {:?}", p.display_name);
}Some(15) Some("Ferris the Crab") Ferris the Crab
still owned by the profile: Some("Ferris the Crab")17. take moves the value out and leaves None
take() is std::mem::take for Option: it gives you the current value and leaves None behind. It is perfect for "use this once" values.
struct Job {
on_complete: Option<String>, // a message to send once
}
impl Job {
fn finish(&mut self) {
if let Some(message) = self.on_complete.take() {
println!("notify: {message}");
} else {
println!("already notified");
}
}
}
fn main() {
let mut job = Job { on_complete: Some(String::from("backup finished")) };
job.finish();
job.finish();
}notify: backup finished
already notified18. replace, insert, and get_or_insert_with
fn main() {
let mut current_user: Option<String> = Some(String::from("ana"));
let previous = current_user.replace(String::from("ben"));
println!("switched from {:?} to {:?}", previous, current_user);
let mut cache: Option<Vec<u64>> = None;
let primes = cache.get_or_insert_with(|| {
println!(" computing primes once...");
vec![2, 3, 5, 7, 11]
});
println!("{:?}", primes);
let again = cache.get_or_insert_with(|| vec![]);
println!("{:?}", again);
}switched from Some("ana") to Some("ben")
computing primes once...
[2, 3, 5, 7, 11]
[2, 3, 5, 7, 11]19. Combining options: or, or_else, zip, xor
fn main() {
let from_env: Option<&str> = None;
let from_file: Option<&str> = Some("config.toml value");
let chosen = from_env.or(from_file).unwrap_or("built-in default");
println!("{chosen}");
let width = Some(1920);
let height = Some(1080);
let missing: Option<i32> = None;
println!("{:?}", width.zip(height));
println!("{:?}", width.zip(missing));
println!("{:?}", Some(1).xor(None::<i32>));
println!("{:?}", Some(1).xor(Some(2)));
}config.toml value
Some((1920, 1080))
None
Some(1)
None20. Optional struct fields
This fixes the "empty string means unchanged" hack from Structs. None means "not provided", and the type makes that explicit.
#[derive(Debug)]
struct Profile {
display_name: String,
website: Option<String>,
newsletter: bool,
}
#[derive(Debug, Default)]
struct ProfileUpdate {
display_name: Option<String>,
website: Option<Option<String>>, // Some(None) means "remove the website"
newsletter: Option<bool>,
}
impl Profile {
fn apply(&mut self, update: ProfileUpdate) {
if let Some(name) = update.display_name {
self.display_name = name;
}
if let Some(website) = update.website {
self.website = website;
}
if let Some(flag) = update.newsletter {
self.newsletter = flag;
}
}
}
fn main() {
let mut p = Profile {
display_name: String::from("ferris"),
website: Some(String::from("https://rust-lang.org")),
newsletter: false,
};
p.apply(ProfileUpdate { newsletter: Some(true), ..Default::default() });
println!("{:?}", p);
p.apply(ProfileUpdate { website: Some(None), display_name: Some(String::from("Ferris")), ..Default::default() });
println!("{:?}", p);
}Profile { display_name: "ferris", website: Some("https://rust-lang.org"), newsletter: true }
Profile { display_name: "Ferris", website: None, newsletter: true }Option<Option<String>> distinguishes three cases that JSON PATCH APIs need: "leave it" (None), "clear it" (Some(None)), and "set it" (Some(Some(url))).
21. None costs nothing for references and boxes
A reference can never be null, so Rust uses the all-zero bit pattern to represent None. Option<&T> is the same size as &T: safety without any memory cost. This is called the null pointer optimization.
fn main() {
println!("&u64: {} bytes", std::mem::size_of::<&u64>());
println!("Option<&u64>: {} bytes", std::mem::size_of::<Option<&u64>>());
println!("Box<u64>: {} bytes", std::mem::size_of::<Box<u64>>());
println!("Option<Box<u64>>: {} bytes", std::mem::size_of::<Option<Box<u64>>>());
println!("u64: {} bytes", std::mem::size_of::<u64>());
println!("Option<u64>: {} bytes", std::mem::size_of::<Option<u64>>());
}&u64: 8 bytes
Option<&u64>: 8 bytes
Box<u64>: 8 bytes
Option<Box<u64>>: 8 bytes
u64: 8 bytes
Option<u64>: 16 bytesOption<u64> does need a tag, because every bit pattern of a u64 is a valid number.
22. Comparing options
Option values can be compared with ==. For ordering, None is less than any Some.
fn main() {
println!("{}", Some(3) == Some(3));
println!("{}", Some(3) == None);
println!("{}", None::<i32> < Some(i32::MIN));
let mut deadlines = vec![Some(30), None, Some(7), None, Some(14)];
deadlines.sort();
println!("{:?}", deadlines);
}true
false
true
[None, None, Some(7), Some(14), Some(30)]23. A for loop over an Option
An Option behaves like a collection of zero or one elements. This is handy for adding an optional item to a list.
fn main() {
let cc: Option<&str> = Some("manager@example.com");
let bcc: Option<&str> = None;
let mut recipients = vec!["team@example.com"];
for extra in cc {
recipients.push(extra);
}
recipients.extend(bcc);
println!("{:?}", recipients);
}["team@example.com", "manager@example.com"]24. Collecting Options from a list
flatten() drops the Nones from a sequence of options. collect() into an Option<Vec<T>> gives None if any element was None, which is perfect for "all or nothing" parsing.
fn main() {
let inputs = ["10", "x", "30"];
let valid: Vec<i32> = inputs.iter().map(|s| s.parse().ok()).flatten().collect();
println!("valid ones: {:?}", valid);
let all: Option<Vec<i32>> = inputs.iter().map(|s| s.parse().ok()).collect();
println!("all or nothing: {:?}", all);
let good = ["1", "2", "3"];
let all_good: Option<Vec<i32>> = good.iter().map(|s| s.parse().ok()).collect();
println!("all or nothing: {:?}", all_good);
}valid ones: [10, 30]
all or nothing: None
all or nothing: Some([1, 2, 3])25. Realistic example: configuration with layered fallbacks
Look up a setting in the command line, then the environment, then the config file, then fall back to a default. Each source may not have it.
fn lookup<'a>(pairs: &[(&str, &'a str)], key: &str) -> Option<&'a str> {
for (k, v) in pairs {
if *k == key {
return Some(v);
}
}
None
}
fn setting<'a>(key: &str, cli: &[(&str, &'a str)], env: &[(&str, &'a str)], file: &[(&str, &'a str)], default: &'a str) -> &'a str {
lookup(cli, key)
.or_else(|| lookup(env, key))
.or_else(|| lookup(file, key))
.unwrap_or(default)
}
fn main() {
let cli = [("log_level", "trace")];
let env = [("db_url", "postgres://prod"), ("log_level", "warn")];
let file = [("db_url", "postgres://localhost"), ("workers", "8")];
for key in ["log_level", "db_url", "workers", "timeout"] {
println!("{key:>9} = {}", setting(key, &cli, &env, &file, "(default)"));
}
}log_level = trace
db_url = postgres://prod
workers = 8
timeout = (default)The 'a lifetime annotations say the returned &str comes from the lookup tables (or the default), not from key. The Lifetimes section explains them.
26. Realistic example: parsing a time of day
Each step can fail, and ? keeps the code as short as the happy path.
fn minutes_since_midnight(time: &str) -> Option<u32> {
let (h, m) = time.trim().split_once(':')?;
let hours: u32 = h.parse().ok()?;
let minutes: u32 = m.parse().ok()?;
if hours > 23 || minutes > 59 {
return None;
}
Some(hours * 60 + minutes)
}
fn main() {
for t in ["09:30", "23:59", "24:00", "7:5", "noon", "12-00"] {
println!("{t:>6} -> {:?}", minutes_since_midnight(t));
}
} 09:30 -> Some(570)
23:59 -> Some(1439)
24:00 -> None
7:5 -> Some(425)
noon -> None
12-00 -> None27. Realistic example: walking a management chain
Each employee may have a manager. Following Option links until None finds the top of the organization.
struct Employee {
id: u32,
name: &'static str,
manager_id: Option<u32>,
}
fn find(staff: &[Employee], id: u32) -> Option<&Employee> {
staff.iter().find(|e| e.id == id)
}
fn chain_of_command(staff: &[Employee], id: u32) -> Vec<&'static str> {
let mut chain = Vec::new();
let mut current = find(staff, id);
while let Some(e) = current {
chain.push(e.name);
current = e.manager_id.and_then(|m| find(staff, m));
}
chain
}
fn main() {
let staff = [
Employee { id: 1, name: "CEO Carol", manager_id: None },
Employee { id: 2, name: "VP Victor", manager_id: Some(1) },
Employee { id: 3, name: "Lead Lina", manager_id: Some(2) },
Employee { id: 4, name: "Dev Dan", manager_id: Some(3) },
];
println!("{}", chain_of_command(&staff, 4).join(" -> "));
println!("{:?}", chain_of_command(&staff, 99));
}Dev Dan -> Lead Lina -> VP Victor -> CEO Carol
[]while let Some(e) = current loops as long as there is a current employee. staff.iter().find(...) returns the first element matching a closure, as an Option.
28. Realistic example: statistics that may not exist
The average of an empty list does not exist. Returning Option<f64> is more honest than returning 0.0 or NaN.
fn average(values: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
let sum: f64 = values.iter().sum();
Some(sum / values.len() as f64)
}
fn median(values: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mid = sorted.len() / 2;
Some(if sorted.len() % 2 == 0 { (sorted[mid - 1] + sorted[mid]) / 2.0 } else { sorted[mid] })
}
fn main() {
let latencies = [120.0, 80.0, 310.0, 95.0];
let none: [f64; 0] = [];
println!("avg {:?}, median {:?}", average(&latencies), median(&latencies));
println!("avg {:?}, median {:?}", average(&none), median(&none));
let report = average(&none).map(|a| format!("{a:.1} ms")).unwrap_or_else(|| String::from("no data"));
println!("{report}");
}avg Some(151.25), median Some(107.5)
avg None, median None
no datapartial_cmp compares floats, which cannot be sorted with plain sort() because of NaN; Deriving Standard Traits explains why.
29. Realistic example: a lazily loaded resource
An Option field starts as None and is filled on first use. Later uses reuse it.
struct ReportService {
template: Option<String>,
loads: u32,
}
impl ReportService {
fn template(&mut self) -> &str {
if self.template.is_none() {
self.loads += 1;
self.template = Some(String::from("Report for {name}: {total}"));
}
self.template.as_deref().unwrap()
}
fn render(&mut self, name: &str, total: u32) -> String {
self.template().replace("{name}", name).replace("{total}", &total.to_string())
}
}
fn main() {
let mut service = ReportService { template: None, loads: 0 };
println!("{}", service.render("north", 120));
println!("{}", service.render("south", 95));
println!("template loaded {} time(s)", service.loads);
}Report for north: 120
Report for south: 95
template loaded 1 time(s)The unwrap() is safe here: the line above guarantees the field is Some. get_or_insert_with from example 18 expresses the same thing in one call.
30. Realistic example: finding the next free slot
fn next_free_slot(booked: &[bool], preferred: usize) -> Option<usize> {
if booked.is_empty() {
return None;
}
let start = preferred.min(booked.len() - 1);
for offset in 0..booked.len() {
let slot = (start + offset) % booked.len();
if !booked[slot] {
return Some(slot);
}
}
None
}
fn main() {
let rooms = [true, false, true, true, false];
println!("{:?}", next_free_slot(&rooms, 2));
println!("{:?}", next_free_slot(&rooms, 0));
println!("{:?}", next_free_slot(&[true, true], 1));
println!("{:?}", next_free_slot(&[], 0));
}Some(4)
Some(1)
None
NoneSummary
Option<T>is eitherSome(value)orNone. There is no null, and anOption<T>cannot be used as aTby accident.- Handle it with
match,if let,let ... else, or?(in functions that returnOption). - Avoid
unwrapin real code; preferexpectwith a reason,unwrap_or,unwrap_or_else, orunwrap_or_default. - Transform with
map,and_then, andfilter; combine withor,or_else, andzip. - Use
as_refandas_derefto look inside without moving, andtakeandreplaceto move values out of&mut Option. Option<&T>andOption<Box<T>>cost no extra memory.
Next: Pattern Matching.
