8.0 What This Teaches
- Defining enums with plain and data-carrying variants
matchfor exhaustive dispatch on an enumimplblocks on enumsOption<T>- Rust's replacement for nullOptionmethods:unwrap_or,mapif letfor single-variant matching- Match guards for extra conditions inside arms
8.1 Basic Enum
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
}
let dir = Direction::North;
match selects a branch based on which variant the value holds. The
compiler requires every variant to be covered - leave one out and it is a compile error:
let msg = match dir {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
};
println!("{msg}");
match over
if/else chains. The compiler tells you at build time when you have forgotten a case.
8.2 Data-Carrying Variants
#[derive(Debug)]
enum Shape {
Circle(f64), // tuple variant: one field (radius)
Rectangle(f64, f64), // tuple variant: two fields (width, height)
Triangle { base: f64, height: f64 }, // struct variant: named fields
}
let c = Shape::Circle(3.0);
let r = Shape::Rectangle(4.0, 5.0);
let t = Shape::Triangle { base: 6.0, height: 4.0 };
match shape {
Shape::Circle(r) => println!("circle, radius {r}"),
Shape::Rectangle(w, h) => println!("rect {w}x{h}"),
Shape::Triangle { base, height } => println!("triangle, base {base}"),
}
8.3 impl Blocks on Enums
match self inside a
method is the standard pattern for dispatching on the variant:
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
let shapes = vec![
Shape::Circle(3.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle { base: 6.0, height: 4.0 },
];
for s in &shapes {
println!("{:?} => area = {:.2}", s, s.area());
}
Circle(3.0) => area = 28.27
Rectangle(4.0, 5.0) => area = 20.00
Triangle { base: 6.0, height: 4.0 } => area = 12.00
8.4 Option<T> - No Null Pointers
Option<T> is a standard library enum defined as:
enum Option<T> {
Some(T),
None,
}
Option.
The compiler forces you to handle both cases before using the inner value - a missing
value can never silently become a crash at runtime.
fn find_first_even(nums: &[i32]) -> Option<i32> {
for &n in nums {
if n % 2 == 0 { return Some(n); }
}
None
}
match to handle both outcomes:match find_first_even(&[1, 3, 5, 8, 9]) {
Some(n) => println!("first even: {n}"),
None => println!("no even number found"),
}
8.5 Option Methods
Option provides convenience methods so you do not always need a full
match.
unwrap_or(default) returns the inner value, or a default if
None:
let val = find_first_even(&nums).unwrap_or(-1);
println!("{val}"); // 8 if found, -1 if not
map(closure) transforms the inner value if Some, passes
None through:
let doubled = find_first_even(&nums).map(|n| n * 2);
println!("{doubled:?}"); // Some(16) or None
is_some() / is_none() give boolean checks without
destructuring:
if find_first_even(&nums).is_some() {
println!("found one");
}
8.6 if let - Single-Variant Match
if let is more concise than a full match:
if let Some(n) = find_first_even(&nums) {
println!("found even: {n}");
}
// None is silently ignored
else when you need to handle the other case:if let Some(n) = find_first_even(&nums) {
println!("found: {n}");
} else {
println!("not found");
}
if let when the logic for one variant is the point and the other
is trivial. Use match when every variant carries meaningful logic.
8.7 Match Guards
if condition called a guard. The arm only
fires when both the pattern matches and the guard is true:
let number = 7;
match number {
n if n < 0 => println!("{n} is negative"),
0 => println!("zero"),
n if n < 10 => println!("{n} is a small positive"),
n => println!("{n} is large"),
}
7 is a small positive
8.8 Example - All Together
// Enums - demonstrates enum variants, data-carrying variants, match, Option, and if let.
#[derive(Debug)]
#[allow(dead_code)]
enum Direction { North, South, East, West }
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
fn find_first_even(nums: &[i32]) -> Option<i32> {
for &n in nums { if n % 2 == 0 { return Some(n); } }
None
}
fn main() {
let dir = Direction::North;
let msg = match dir {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
};
println!("{msg}");
let shapes = vec![
Shape::Circle(3.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle { base: 6.0, height: 4.0 },
];
for s in &shapes { println!("{:?} => area = {:.2}", s, s.area()); }
let nums = vec![1, 3, 5, 8, 9];
match find_first_even(&nums) {
Some(n) => println!("first even: {n}"),
None => println!("not found"),
}
let val = find_first_even(&nums).unwrap_or(-1);
let doubled = find_first_even(&nums).map(|n| n * 2);
println!("unwrap_or: {val}, map: {doubled:?}");
if let Some(n) = find_first_even(&nums) {
println!("if let found: {n}");
}
let number = 7;
match number {
n if n < 0 => println!("{n} is negative"),
0 => println!("zero"),
n if n < 10 => println!("{n} is a small positive"),
n => println!("{n} is large"),
}
}
heading north
Circle(3.0) => area = 28.27
Rectangle(4.0, 5.0) => area = 20.00
Triangle { base: 6.0, height: 4.0 } => area = 12.00
first even: 8
unwrap_or: 8, map: Some(16)
if let found: 8
7 is a small positive
8.9 Exercise
Exercise
- Define an enum
Coinwith variantsPenny,Nickel,Dime,Quarter. Write a functionvalue_in_cents(c: &Coin) -> u32usingmatch. Call it for each variant and print the results. - Add a variant
Dollar(String)toCoinwhere theStringholds the year of minting. Updatevalue_in_centsto return 100 forDollar, and add a match arm that also prints the year. DeriveDebugand print aDollarinstance. - Write a function
safe_divide(a: f64, b: f64) -> Option<f64>that returnsNonewhenbis zero andSome(a / b)otherwise. Useif letto print the result, and test both cases.
8.10 Common Mistakes
Non-exhaustive match
match dir {
Direction::North => "north",
Direction::South => "south",
// error: patterns `East` and `West` not covered
}
_ to catch the rest.Using unwrap() without checking
let n = find_first_even(&nums).unwrap(); // panics at runtime if None
.unwrap() panics when called on None. Use
.unwrap_or(default), match, or if let
instead until you are certain the value is Some.
Bare name treated as a catch-all binding instead of a variant
match shape {
Circle => println!("circle"), // wrong: Circle is a binding, not a variant path
}
match require the full path
Shape::Circle. A bare name like Circle is treated as a
catch-all binding, making subsequent arms unreachable.
Forgetting to destructure data in a variant arm
match shape {
Shape::Circle => println!("circle"), // error: expected tuple struct or tuple variant
}
Shape::Circle carries a f64. The arm must bind it:
Shape::Circle(r) => ... or Shape::Circle(_) => ...
to discard it.
8.11 Key Terms
| Term | Meaning |
|---|---|
| enum | A type whose value is exactly one of a fixed set of named variants |
| variant | One of the possible forms an enum value can take |
| tuple variant | A variant carrying unnamed positional fields: Circle(f64) |
| struct variant | A variant carrying named fields: Triangle { base, height } |
| match | Exhaustive pattern-matching expression; every variant must be covered |
| match guard | An if condition added to a match arm for extra filtering |
| Option<T> | Standard enum representing a value that may be absent: Some(T) or None |
| if let | Concise syntax for matching a single variant and binding its data |
| unwrap_or | Returns the inner value of Some, or a given default for None |
| map | Transforms the inner value of Some via a closure; passes None through |