Rust - Enums

Overview

Estimated time: 55–75 minutes

Learn how to define and use enums in Rust, including pattern matching with match expressions. Enums are powerful tools for expressing data that can be one of several variants.

Learning Objectives

Prerequisites

Basic Enums

Enums allow you to define types that can be one of several variants:

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let direction = Direction::North;
    
    match direction {
        Direction::North => println!("Going north!"),
        Direction::South => println!("Going south!"),
        Direction::East => println!("Going east!"),
        Direction::West => println!("Going west!"),
    }
}

Expected output:

Going north!

Enums with Data

Enum variants can hold different types and amounts of data:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("Quit message received");
        }
        Message::Move { x, y } => {
            println!("Move to coordinates ({}, {})", x, y);
        }
        Message::Write(text) => {
            println!("Text message: {}", text);
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to RGB({}, {}, {})", r, g, b);
        }
    }
}

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello, world!")),
        Message::ChangeColor(255, 0, 128),
    ];
    
    for message in messages {
        process_message(message);
    }
}

Expected output:

Quit message received
Move to coordinates (10, 20)
Text message: Hello, world!
Change color to RGB(255, 0, 128)

The Option Enum

Option is used to represent values that might be absent, replacing null pointers:

fn find_word(text: &str, word: &str) -> Option {
    text.find(word)
}

fn divide(x: f64, y: f64) -> Option {
    if y != 0.0 {
        Some(x / y)
    } else {
        None
    }
}

fn main() {
    let text = "Hello, world!";
    
    match find_word(text, "world") {
        Some(index) => println!("Found 'world' at index {}", index),
        None => println!("'world' not found"),
    }
    
    match find_word(text, "rust") {
        Some(index) => println!("Found 'rust' at index {}", index),
        None => println!("'rust' not found"),
    }
    
    // Using Option with arithmetic
    let result1 = divide(10.0, 2.0);
    let result2 = divide(10.0, 0.0);
    
    match result1 {
        Some(value) => println!("10.0 / 2.0 = {}", value),
        None => println!("Division by zero!"),
    }
    
    match result2 {
        Some(value) => println!("10.0 / 0.0 = {}", value),
        None => println!("Division by zero!"),
    }
}

Expected output:

Found 'world' at index 7
'rust' not found
10.0 / 2.0 = 5
Division by zero!

Working with Option

Common patterns for working with Option values:

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option = None;
    
    // Using if let for simple cases
    if let Some(value) = some_number {
        println!("Got a value: {}", value);
    }
    
    // Using unwrap_or for default values
    let x = absent_number.unwrap_or(0);
    println!("Value or default: {}", x);
    
    // Using map to transform values
    let doubled = some_number.map(|n| n * 2);
    println!("Doubled: {:?}", doubled);
    
    // Chaining operations
    let result = some_number
        .map(|n| n * 3)
        .map(|n| n + 1)
        .unwrap_or(0);
    println!("Chained operations: {}", result);
}

Expected output:

Got a value: 5
Value or default: 0
Doubled: Some(10)
Chained operations: 16

Pattern Matching

Match expressions provide powerful pattern matching capabilities:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String), // Quarter with state name
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {}!", state);
            25
        }
    }
}

fn main() {
    let coins = vec![
        Coin::Penny,
        Coin::Nickel,
        Coin::Dime,
        Coin::Quarter(String::from("Alaska")),
    ];
    
    let mut total = 0;
    for coin in coins {
        total += value_in_cents(coin);
    }
    
    println!("Total value: {} cents", total);
}

Expected output:

Lucky penny!
State quarter from Alaska!
Total value: 41 cents

Advanced Pattern Matching

enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
    Kelvin(f64),
}

fn describe_temperature(temp: Temperature) {
    match temp {
        Temperature::Celsius(c) if c < 0.0 => {
            println!("{:.1}°C - Freezing!", c);
        }
        Temperature::Celsius(c) if c > 30.0 => {
            println!("{:.1}°C - Hot!", c);
        }
        Temperature::Celsius(c) => {
            println!("{:.1}°C - Comfortable", c);
        }
        Temperature::Fahrenheit(f) => {
            let celsius = (f - 32.0) * 5.0 / 9.0;
            println!("{:.1}°F ({:.1}°C)", f, celsius);
        }
        Temperature::Kelvin(k) => {
            let celsius = k - 273.15;
            println!("{:.1}K ({:.1}°C)", k, celsius);
        }
    }
}

fn main() {
    let temperatures = vec![
        Temperature::Celsius(-5.0),
        Temperature::Celsius(25.0),
        Temperature::Celsius(35.0),
        Temperature::Fahrenheit(100.0),
        Temperature::Kelvin(300.0),
    ];
    
    for temp in temperatures {
        describe_temperature(temp);
    }
}

Expected output:

-5.0°C - Freezing!
25.0°C - Comfortable
35.0°C - Hot!
100.0°F (37.8°C)
300.0K (26.9°C)

The Result Enum

Result is used for operations that might fail:

use std::num::ParseIntError;

fn parse_number(s: &str) -> Result {
    s.parse()
}

fn divide_strings(dividend: &str, divisor: &str) -> Result {
    let num1: f64 = dividend.parse()
        .map_err(|_| format!("Invalid dividend: '{}'", dividend))?;
    
    let num2: f64 = divisor.parse()
        .map_err(|_| format!("Invalid divisor: '{}'", divisor))?;
    
    if num2 == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(num1 / num2)
    }
}

fn main() {
    // Working with parse results
    let inputs = vec!["42", "abc", "0", "-5"];
    
    for input in inputs {
        match parse_number(input) {
            Ok(num) => println!("'{}' parsed as: {}", input, num),
            Err(e) => println!("Failed to parse '{}': {}", input, e),
        }
    }
    
    // Working with complex Results
    let operations = vec![
        ("10", "2"),
        ("15", "3"),
        ("8", "0"),
        ("abc", "5"),
    ];
    
    for (dividend, divisor) in operations {
        match divide_strings(dividend, divisor) {
            Ok(result) => println!("{} / {} = {:.2}", dividend, divisor, result),
            Err(error) => println!("Error: {}", error),
        }
    }
}

Expected output:

'42' parsed as: 42
Failed to parse 'abc': invalid digit found in string
'0' parsed as: 0
'-5' parsed as: -5
10 / 2 = 5.00
15 / 3 = 5.00
Error: Division by zero
Error: Invalid dividend: 'abc'

Methods on Enums

You can implement methods on enums just like structs:

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(radius) => {
                std::f64::consts::PI * radius * radius
            }
            Shape::Rectangle(width, height) => {
                width * height
            }
            Shape::Triangle(a, b, c) => {
                // Using Heron's formula
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
    
    fn perimeter(&self) -> f64 {
        match self {
            Shape::Circle(radius) => {
                2.0 * std::f64::consts::PI * radius
            }
            Shape::Rectangle(width, height) => {
                2.0 * (width + height)
            }
            Shape::Triangle(a, b, c) => {
                a + b + c
            }
        }
    }
    
    fn describe(&self) -> String {
        match self {
            Shape::Circle(radius) => {
                format!("Circle with radius {:.1}", radius)
            }
            Shape::Rectangle(width, height) => {
                format!("Rectangle {}×{}", width, height)
            }
            Shape::Triangle(a, b, c) => {
                format!("Triangle with sides {:.1}, {:.1}, {:.1}", a, b, c)
            }
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(4.0, 6.0),
        Shape::Triangle(3.0, 4.0, 5.0),
    ];
    
    for shape in shapes {
        println!("{}", shape.describe());
        println!("  Area: {:.2}", shape.area());
        println!("  Perimeter: {:.2}", shape.perimeter());
        println!();
    }
}

Expected output:

Circle with radius 5.0
  Area: 78.54
  Perimeter: 31.42

Rectangle 4×6
  Area: 24.00
  Perimeter: 20.00

Triangle with sides 3.0, 4.0, 5.0
  Area: 6.00
  Perimeter: 12.00

Common Pitfalls

1. Non-exhaustive Match

Match expressions must cover all possible variants:

enum Color {
    Red,
    Green,
    Blue,
}

fn main() {
    let color = Color::Red;
    
    // Wrong: missing Blue variant
    // match color {
    //     Color::Red => println!("Red"),
    //     Color::Green => println!("Green"),
    //     // Missing Color::Blue - compiler error!
    // }
    
    // Correct: handle all variants
    match color {
        Color::Red => println!("Red"),
        Color::Green => println!("Green"),
        Color::Blue => println!("Blue"),
    }
    
    // Or use a catch-all
    match color {
        Color::Red => println!("It's red!"),
        _ => println!("Some other color"),
    }
}

2. Using unwrap() on Option/Result

fn main() {
    let maybe_number: Option = None;
    
    // Dangerous: will panic if None
    // let number = maybe_number.unwrap();
    
    // Better: handle the None case
    match maybe_number {
        Some(num) => println!("Got number: {}", num),
        None => println!("No number available"),
    }
    
    // Or use unwrap_or for a default
    let number = maybe_number.unwrap_or(0);
    println!("Number or default: {}", number);
}

Checks for Understanding

Question 1: What's the difference between Option<T> and Result<T, E>?

Answer: Option<T> represents a value that might be absent (Some(T) or None). Result<T, E> represents an operation that might fail, returning either success (Ok(T)) or an error (Err(E)).

Question 2: What happens if you don't handle all enum variants in a match?

Answer: The Rust compiler will produce a compile-time error. Match expressions must be exhaustive - they must handle all possible variants or use a catch-all pattern like _.

Question 3: How would you define an enum for different HTTP status codes?

Answer:

enum HttpStatus {
    Ok,
    NotFound,
    InternalServerError,
    BadRequest(String), // With error message
}

← PreviousNext →