Module: Structs and Enums

Enums

Rust Enums: A Deep Dive

Enums (short for enumerations) are a powerful feature in Rust that allow you to define a type by enumerating its possible values. They're incredibly useful for representing data that can be one of several distinct options. Think of them as a type that can be one of several different kinds of things.

Why use Enums?

  • Type Safety: Enums enforce that a variable can only hold one of the defined variants, preventing invalid states.
  • Data Modeling: Excellent for representing choices, states, or events.
  • Pattern Matching: Enums work seamlessly with Rust's powerful pattern matching, making code concise and readable.
  • Memory Efficiency: Rust enums are often more memory-efficient than using a tagged union in other languages.

Basic Enum Definition

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

fn main() {
    let north = Direction::North;
    println!("Direction: {:?}", north); // Output: Direction::North
}
  • enum Direction: Declares an enum named Direction.
  • North, South, East, West: These are the variants of the Direction enum. Each variant represents a possible value the enum can hold.
  • Direction::North: How you access a specific variant.

Enums with Data (Payloads)

Enums aren't limited to just simple names. They can also carry data associated with each variant.

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

fn main() {
    let quit = Message::Quit;
    let move_msg = Message::Move { x: 10, y: 20 };
    let write_msg = Message::Write(String::from("Hello, world!"));
    let color_msg = Message::ChangeColor(255, 0, 0);

    println!("Quit: {:?}", quit);        // Output: Quit
    println!("Move: {:?}", move_msg);    // Output: Move { x: 10, y: 20 }
    println!("Write: {:?}", write_msg);   // Output: Write("Hello, world!")
    println!("Color: {:?}", color_msg);   // Output: ChangeColor(255, 0, 0)
}
  • Move { x: i32, y: i32 }: The Move variant carries a struct-like data structure with x and y coordinates.
  • Write(String): The Write variant carries a String.
  • ChangeColor(i32, i32, i32): The ChangeColor variant carries three i32 values representing RGB color components.

Pattern Matching with match

The real power of enums comes into play when combined with Rust's match expression. match allows you to deconstruct enums and handle each variant differently.

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

fn process_message(msg: Message) {
    match msg {
        Message::Quit => {
            println!("The user wants to quit.");
        }
        Message::Move { x, y } => {
            println!("Move the player to x={}, y={}", x, y);
        }
        Message::Write(text) => {
            println!("Write the following text: {}", text);
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change the color to red={}, green={}, blue={}", r, g, b);
        }
    }
}

fn main() {
    let messages = [
        Message::Quit,
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello")),
        Message::ChangeColor(255, 0, 0),
    ];

    for msg in messages {
        process_message(msg);
    }
}
  • match msg { ... }: The match expression takes the msg variable (of type Message) and compares it against each pattern.
  • Message::Quit => { ... }: If msg is the Quit variant, the code inside the curly braces is executed.
  • Message::Move { x, y } => { ... }: If msg is the Move variant, the x and y values are extracted from the variant's data and bound to the variables x and y.
  • Message::Write(text) => { ... }: If msg is the Write variant, the String inside the variant is bound to the variable text.
  • Exhaustiveness: The match expression must cover all possible variants of the enum. If it doesn't, the compiler will give an error. You can use _ (the wildcard pattern) to match any remaining variants.

if let for Concise Matching

If you only need to handle a single variant, if let provides a more concise syntax.

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

fn main() {
    let msg = Message::Move { x: 5, y: 10 };

    if let Message::Move { x, y } = msg {
        println!("Moving to x={}, y={}", x, y);
    } else {
        println!("Not a move message.");
    }
}

Enums as Methods

You can define methods on enums just like you would on structs.

enum Color {
    Red,
    Green,
    Blue,
}

impl Color {
    fn to_rgb(&self) -> (u8, u8, u8) {
        match self {
            Color::Red => (255, 0, 0),
            Color::Green => (0, 255, 0),
            Color::Blue => (0, 0, 255),
        }
    }
}

fn main() {
    let red = Color::Red;
    let rgb = red.to_rgb();
    println!("Red in RGB: {:?}", rgb); // Output: Red in RGB: (255, 0, 0)
}

Key Takeaways

  • Enums are a fundamental part of Rust's type system.
  • They allow you to define types that can be one of several distinct options.
  • Enums can carry data associated with each variant.
  • match expressions are the primary way to work with enums, providing type safety and conciseness.
  • if let offers a more concise syntax for handling a single variant.
  • You can define methods on enums to encapsulate behavior.

Enums are a powerful tool for building robust and expressive Rust applications. Mastering them is crucial for writing idiomatic and maintainable code.