DaleSchool

Multiple Possibilities in One Type: Enums

Intermediate20min

Learning Objectives

  • Create custom types with enum
  • Include data in enum variants
  • Handle all variants with match
  • Use if let to handle a single variant

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

  1. Change light to TrafficLight::Green. Does the output change?
  2. Delete the TrafficLight::Yellow arm from the match. 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