Working Code
Think about a traffic light. It can only be red, yellow, or green — one state at a time. This "one of several possibilities" idea is what enums express.
#[derive(Debug)]
enum TrafficLight {
Red,
Yellow,
Green,
}
fn action(light: &TrafficLight) {
match light {
TrafficLight::Red => println!("Stop!"),
TrafficLight::Yellow => println!("Caution!"),
TrafficLight::Green => println!("Go!"),
}
}
fn main() {
let light = TrafficLight::Red;
action(&light);
println!("Current light: {:?}", light);
}
match looks at the value and runs different code depending on which variant it is. Think of it as a sorting machine — you feed in a value and it routes it to the right slot based on its shape.
Try It Yourself
- Change
lighttoTrafficLight::Green. Does the output change? - Delete the
TrafficLight::Yellowarm from thematch. What error do you get?
"Why?" — Enums With Data
The real power of enums is that each variant can carry data:
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64), // three side lengths
}
fn describe(shape: &Shape) {
match shape {
Shape::Circle(r) => {
println!("Circle: radius {r}");
}
Shape::Rectangle(w, h) => {
println!("Rectangle: {w} x {h}");
}
Shape::Triangle(a, b, c) => {
println!("Triangle: sides {a}, {b}, {c}");
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(10.0, 3.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for s in &shapes {
describe(s);
}
}
A single Shape type can hold circles, rectangles, and triangles. This would be hard to express with structs alone.
match Must Be Exhaustive
The most important trait of match is that it must handle every possible variant (exhaustive matching).
error[E0004]: non-exhaustive patterns: `Shape::Triangle(_, _, _)`
not covered
What it means: "You didn't handle the Triangle case. match must cover every possibility."
Fix: Add the missing variant, or use _ (wildcard) to handle the rest.
When handling every case individually is tedious, use _ to catch the remaining variants:
fn is_circle(shape: &Shape) -> bool {
match shape {
Shape::Circle(_) => true,
_ => false, // everything else is false
}
}
if let — When You Only Care About One
If you only want to check for one variant and ignore the rest, if let is cleaner:
fn main() {
let shape = Shape::Circle(5.0);
// With match
match &shape {
Shape::Circle(r) => println!("Radius: {r}"),
_ => {}
}
// With if let — much cleaner!
if let Shape::Circle(r) = &shape {
println!("Radius: {r}");
}
}
Enums Can Have Methods Too
Just like structs, you can add methods with an impl block:
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(a, b, c) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
}
Learn More: Option — Rust Has No null
Other languages use null or None to represent "no value." But mishandling null crashes programs all the time.
Rust has no null! Instead, it uses the Option enum:
enum Option<T> {
Some(T), // a value exists
None, // no value
}
Remember when pop() returned Some(88) in Lesson 06? That was Option!
fn main() {
let numbers = vec![1, 2, 3];
let first = numbers.first(); // Option<&i32>
match first {
Some(n) => println!("First: {n}"),
None => println!("Empty"),
}
}
You'll learn more about Option later. For now, know that Rust uses enums to safely represent situations where a value might not exist.
Exercise 1. Create a Coin enum and write a value method that returns each coin's amount.
#[derive(Debug)]
enum Coin {
Penny, // 1 cent
Nickel, // 5 cents
Dime, // 10 cents
Quarter, // 25 cents
}
impl Coin {
fn value(&self) -> i32 {
// Complete this
// Use match to return the right amount for each variant
}
}
fn main() {
let coins = vec![
Coin::Dime,
Coin::Quarter,
Coin::Nickel,
];
let total: i32 = coins.iter().map(|c| c.value()).sum();
println!("Total: {total} cents");
// Output: Total: 40 cents
}
Exercise 2. Complete the process function for the Message enum below.
#[derive(Debug)]
enum Message {
Quit,
Echo(String),
Move { x: i32, y: i32 },
}
fn process(msg: &Message) {
// Complete this
// Quit -> print "Quitting"
// Echo(text) -> print the text
// Move { x, y } -> print "Moving to ({x}, {y})"
}
fn main() {
let messages = vec![
Message::Echo(String::from("hello!")),
Message::Move { x: 10, y: 20 },
Message::Quit,
];
for msg in &messages {
process(msg);
}
}
Hint: Destructure a struct variant with Message::Move { x, y } in the match arm.
Q1. What happens if you don't handle all variants in a match?
- A) A runtime error occurs
- B) A compile error occurs
- C) Unhandled variants are silently ignored
- D) The program enters an infinite loop
Q2. When is if let a good choice?
- A) When you need to handle every variant
- B) When you only care about one variant and want to ignore the rest
- C) When creating an enum
- D) When adding methods to an enum
Q3. What does this code print?
enum Direction {
Up, Down, Left, Right,
}
fn main() {
let dir = Direction::Up;
let msg = match dir {
Direction::Up => "up",
Direction::Down => "down",
_ => "side",
};
println!("{msg}");
}
- A) "up"
- B) "down"
- C) "side"
- D) Compile error