12. Enums + Introducing Option<T>
Rust enums are a different beast from C enums — each variant can carry **different data of different types**. They're true sum types. The most-used enum, `Option<T>` (`Some(T)` or `None`), is how Rust avoids the billion-dollar null-pointer mistake.
What you'll learn
- 1Attach different payload types to enum variants
- 2Express optional values with `Option<T>` instead of null
- 3Destructure enums with `match` and `if let`
- 4Use `.unwrap()` / `.unwrap_or()` / `.map()` on Option
- 5Explain how a null-free language is safer
Overview
In 2009 Tony Hoare called the 1965 invention of null pointers his "billion-dollar mistake." Rust removes null from the language entirely and replaces it with `Option<T>` — the **type system makes absence explicit**, so the compiler catches forgotten checks.
Core Concepts
1) Rust enums are sum types
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named struct payload
Write(String), // tuple of 1
ChangeColor(i32, i32, i32), // tuple of 3
}Each variant is essentially "one case" of a different shape. Not just integer aliases like C enums.
2) Option<T> is built in
enum Option<T> {
None,
Some(T),
}It's in the prelude, so you write `Option<T>` and `Some(...)` / `None` without imports.
3) Destructuring — match / if let
- **match** — every variant must be handled (compile error if you miss one)
- **if let** — short form when you only care about one variant
4) Option helpers
| Method | Meaning |
|---|---|
| .unwrap() | Some → inner, None → panic |
| .unwrap_or(d) | Some → inner, None → d |
| .map(|x| ...) | Transform Some only |
| .and_then(|x| ...) | Transform Some, return Option |
| .is_some() / .is_none() | Boolean checks |
Hands-on Examples
match with full variant coverage:
enum Coin { Penny, Nickel, Dime, Quarter }
fn value(c: Coin) -> u32 {
match c {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() { println!("{}", value(Coin::Quarter)); }Working with Option<T>:
fn find_first_even(v: &[i32]) -> Option<i32> {
for &x in v {
if x % 2 == 0 { return Some(x); }
}
None
}
fn main() {
let v = [1, 3, 4, 7, 8];
match find_first_even(&v) {
Some(x) => println!("found {}", x),
None => println!("not found"),
}
if let Some(x) = find_first_even(&v) { println!("got {}", x); }
let v2: [i32; 0] = [];
let result = find_first_even(&v2).unwrap_or(-1);
println!("{}", result); // -1
}Data-carrying variants:
enum Shape {
Circle(f64),
Rectangle { w: f64, h: f64 },
}
fn area(s: Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle { w, h } => w * h,
}
}
fn main() {
println!("{}", area(Shape::Circle(2.0)));
println!("{}", area(Shape::Rectangle { w: 3.0, h: 4.0 }));
}Common Mistakes
Q. Can I just use .unwrap() everywhere?
A. Fine for prototypes / tests. Real code should prefer `.unwrap_or(...)` / `.map` / the `?` operator (coming up) to avoid runtime panics in failure paths.
Q. Match is verbose. Can I use a wildcard?
A. `_ =>` matches everything else. But explicit variants are safer — when you add a new variant later, the compiler will tell you which match arms need updating.
Q. Is the no-null thing really that big a deal?
A. In Java/Python the NullPointerException happens at runtime. Rust makes `None` impossible to silently pass in place of `T` — handling absence becomes a compile-time requirement.
Recap
- Each enum variant can hold different data (sum type)
- `Option<T>` replaces null — absence is encoded in the type
- Destructure with match (exhaustive) or if let (single case)
- Useful helpers: .unwrap / .unwrap_or / .map
Try It Yourself
- Write `fn find(v: &[i32], target: i32) -> Option<usize>`
- Define `enum Event { Login(String), Logout, Click { x: i32, y: i32 } }` and match each variant to a different message
- Write a function adding two Option<i32>: both Some → Some(sum), any None → None
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗