Rust has no null. Instead, when a value might be absent, the type makes that
explicit using Option<T>:
enum Option<T> {
Some(T),
None,
}
The compiler will not let you accidentally use a None as if it were a real
value. To extract the inner value, you need to decide what to do if it is
missing.
There are two main ways to unwrap an option. You can spell out both cases with pattern matching:
match find_user(id) {
Some(name) => println!("found {name}"),
None => println!("no such user"),
}
But for common cases there are shorter combinators:
let port = settings.port.unwrap_or(8080); // value or fallback
let upper = name.map(|s| s.to_uppercase()); // transform if Some
let len = maybe_str.map_or(0, |s| s.len()); // transform-or-default
|x| ... (Closures)Those |s| s.to_uppercase() and |s| s.len() bits are closures: anonymous
functions you can pass as arguments. The pipes hold the parameters; everything
after them is the body:
let add_one = |x| x + 1;
add_one(2); // 3
If the body needs multiple statements, wrap it in braces:
let greet = |name: &str| {
let trimmed = name.trim();
format!("hello, {trimmed}")
};
We'll use more closures in the iterators chapter. For this chapter, just read
|s| s.len() as "a small function that takes s and returns s.len()."
When you only need to handle Some, you can use if let instead of a full
match:
if let Some(user) = find_user(id) {
println!("welcome, {user}");
}
Many standard-library methods return Option: .first() and .last() on
slices, .next() and .find(...) on iterators, and .get() on slices and
maps.
You need the string's length in bytes when the Option is Some, and 0 when
it's None. That means calling .len() on the inner string before returning
the result. A match makes both branches explicit, while Option's combinator
methods keep this common case shorter.
Useful from the Standard Library
Option::mapapplies a function inside theSomeand leavesNonealone.Option::map_orhandles both cases in one call, with a default forNoneand a closure forSome.
/// Returns the length if `Some`, 0 if `None`.
fn optional_string_length(maybe_string: Option<&str>) -> usize {
maybe_string.map_or(0, |s| s.len())
}
#[test]
fn test_optional_string_length() {
assert_eq!(optional_string_length(Some("hello")), 5);
assert_eq!(optional_string_length(None), 0);
}
Now you have to produce an Option, not consume one. If the string is empty,
there is no first character; Option<char> represents that case with None.
text.chars() returns an iterator, and every iterator's .next() returns
Option<Item>. Call .next() on the iterator to get the first character, if
there is one.
Useful from the Standard Library
str::charsreturns an iterator over thechars of the string.Iterator::nextpulls one item off the iterator and returns it asOption<Item>. For the first character, that'sOption<char>and exactly the return type.
/// Returns the first character of `text`, or `None` if the string is empty.
fn first_char(text: &str) -> Option<char> {
text.chars().next()
}
#[test]
fn test_first_char() {
assert_eq!(first_char("hello"), Some('h'));
assert_eq!(first_char("rust"), Some('r'));
assert_eq!(first_char(""), None);
}
Produce an Option by searching a slice of user records. The search returns a
reference to a whole (u32, String) tuple, while the function must return only
the username as Option<&str>. You need to borrow the username as &str
without cloning the String. If the chain of calls gets hard to follow, pause
after find and work out which type you have before adding map.
Useful from the Standard Library
<[T]>::iteryields shared references to the slice's items, one at a time.Iterator::findtakes a predicate closure and returns the first matching item as anOption.Option::maptransforms the inner value when present. Here it pulls the username out of the tuple and converts it to&str.String::as_stris the explicit "borrow thisStringas&str" call.
/// Finds a user by ID. Returns `Some(username)` if found, `None` if not.
fn find_user_by_id(users: &[(u32, String)], id: u32) -> Option<&str> {
users
.iter()
.find(|(uid, _)| *uid == id)
.map(|(_, name)| name.as_str())
}
#[test]
fn test_find_user_by_id() {
let users = [
(1, "alice".to_string()),
(2, "bob".to_string()),
(3, "charlie".to_string()),
];
assert_eq!(find_user_by_id(&users, 2), Some("bob"));
assert_eq!(find_user_by_id(&users, 99), None);
}
You consumed Options with fallbacks and combinators, produced new ones from
string and slice operations, and chained find and map to turn a search into
the exact return type the signature asked for.
What We Learned
Option<T>is Rust's stand-in for "value or absence". You cannot use it as aTwithout first extracting the value.- You can use
matchto spell out both cases. For common patterns, reach forunwrap_or,map, andmap_orto keep call sites short.if let Some(x) = ...is the lighter alternative tomatchwhen you only care about theSomebranch.Option::mapmirrors the iterator method of the same name: it transforms the inside if present, leavesNonealone.- Many standard-library operations already return
Option:iter().next(),slice.first(),Vec::pop,HashMap::get,iter().find(...).unwrapandexpectextract the value but panic onNone. Use them in tests or when you've already ruled outNone; otherwise prefer the safer combinators.- The
|x| ...syntax is a closure: a tiny anonymous function. It shows up everywhere withOptionand iterators. We'll cover closures in more detail later.
Extra practice to explore at your own pace. These chapters do not count toward course progress.