10.2 Basic Enums: Enumerating Possibilities

The simplest Rust enums closely resemble C enums, defining a set of named variants without associated data. These are often called “C-like enums” or “fieldless enums”.

10.2.1 Rust Example: Simple Enum

// Define an enum named Direction with four variants
#[derive(Debug, PartialEq, Eq, Clone, Copy)] // Add traits for comparison, copy, print
enum Direction {
    North,
    East,
    South,
    West,
}

fn print_direction(heading: Direction) {
    // Use 'match' to handle each variant
    match heading {
        Direction::North => println!("Heading North"),
        Direction::East  => println!("Heading East"),
        Direction::South => println!("Heading South"),
        Direction::West  => println!("Heading West"),
    }
}

fn main() {
    let current_heading = Direction::North;
    print_direction(current_heading);

    let another_heading = Direction::West;
    print_direction(another_heading);

    if current_heading == Direction::North {
        println!("Confirmed North!");
    }
}
  • Deriving Traits: We added #[derive(Debug, PartialEq, Eq, Clone, Copy)].
    • Debug: Allows printing the enum using {:?}.
    • PartialEq, Eq: Allow comparing variants for equality (e.g., current_heading == Direction::North).
    • Clone, Copy: Allow simple enums like this to be copied easily, like integers (let new_heading = current_heading; makes a copy, not a move). These traits are often derived for C-like enums.
  • Definition: The enum Direction type has four possible values: Direction::North, Direction::East, Direction::South, and Direction::West.
  • Namespacing: Variants are accessed using the enum name followed by :: (e.g., Direction::North). This is the qualified path.
  • Pattern Matching: The match expression is Rust’s primary tool for handling enums. It compares a value against patterns (here, the variants). match requires exhaustiveness – all variants must be handled, ensuring no case is forgotten.

10.2.2 Unqualified Enum Variants with use

While the qualified path (e.g., Direction::North) is the most common and often clearest way to refer to enum variants, Rust allows you to bring variants into the current scope using a use statement. This permits referring to them directly by their variant name (e.g., North).

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Direction {
    North,
    East,
    South,
    West,
}

// Bring specific variants into scope
use Direction::{North, West};

// You can also bring all variants into scope with a wildcard:
// use Direction::*;

fn print_direction_short(heading: Direction) {
    // Now we can use unqualified names in patterns
    match heading {
        North => println!("Heading North (unqualified)"), // No Direction:: prefix
        Direction::East => println!("Heading East (qualified)"), // Can still use qualified
        Direction::South => println!("Heading South (qualified)"),
        West => println!("Heading West (unqualified)"), // No Direction:: prefix
    }
}

fn main() {
    // Unqualified names can be used for assignment too
    let current_heading = North;
    print_direction_short(current_heading);

    let another_heading = West;
    print_direction_short(another_heading);

    // Comparison works with unqualified names too
    if current_heading == North {
        println!("Confirmed North (unqualified comparison)!");
    }
}
  • use Direction::{Variant1, Variant2};: Imports specific variants into the current scope.
  • use Direction::*;: Imports all variants from the Direction enum into the current scope.
  • Clarity vs. Brevity: Using unqualified names can make code shorter, especially within functions or modules that heavily use a particular enum. However, qualified names (Direction::North) are generally preferred in broader scopes or when variant names might clash with other identifiers, as they provide better clarity about the origin of the name.

10.2.3 Comparison with C: Simple Enum

Here’s a similar concept implemented in C:

#include <stdio.h>

// C enum defines named integer constants
enum Direction {
    North, // Typically defaults to 0
    East,  // Typically defaults to 1
    South, // Typically defaults to 2
    West   // Typically defaults to 3
};

void print_direction(enum Direction heading) {
    // Use 'switch' to handle each case
    switch (heading) {
        case North: printf("Heading North\n"); break;
        case East:  printf("Heading East\n");  break;
        case South: printf("Heading South\n"); break;
        case West:  printf("Heading West\n");  break;
        default:    printf("Unknown heading: %d\n", heading); break;
    }
}

int main() {
    enum Direction current_heading = North;
    print_direction(current_heading);

    // C enums are essentially integers
    int invalid_heading_val = 10;
    // This might compile but leads to undefined behavior via the switch default case:
    // print_direction((enum Direction)invalid_heading_val); // Potential issue!

    return 0;
}
  • Definition: C enum variants are aliases for integer constants and are typically used without qualification.
  • Type Safety: C offers weaker type safety. You can often cast arbitrary integers to an enum type.
  • Switch Statement: C’s switch doesn’t enforce exhaustiveness by default.

10.2.4 Assigning Explicit Discriminant Values

Like C, Rust allows you to assign specific integer values (discriminants) to enum variants, often essential for FFI or specific numeric requirements.

// Specify the underlying integer type with #[repr(...)]
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] // Add common derives
enum ErrorCode {
    NotFound = -1,
    PermissionDenied = -2,
    ConnectionFailed = -3,
    // Mix explicit and default assignments (default follows last explicit)
    Timeout = 5, // Explicitly 5
    Unknown,     // Implicitly 6 (5 + 1)
}

fn main() {
    let error = ErrorCode::PermissionDenied;
    // Cast the enum variant to its integer representation
    let error_value = error as i32;
    println!("Error code: {:?}", error);       // Debug print uses the variant name
    println!("Error value: {}", error_value); // Cast gives the integer value

    let code_unknown = ErrorCode::Unknown;
    println!("Unknown code: {:?}", code_unknown);      // Output: Unknown
    println!("Unknown value: {}", code_unknown as i32); // Output: 6
}
  • #[repr(type)]: Specifies the underlying integer type (i32, u8, etc.). Crucial for predictable layout and FFI.
  • Explicit Values: Assign any value of the specified type. Values need not be sequential. Unassigned variants get the previous value + 1.
  • Casting: Use as to explicitly convert a variant to its integer value.

Casting from Integers to Enums (Use with Caution)

Converting an integer back to an enum requires care, as the integer might not correspond to a valid variant. Direct transmute is unsafe and highly discouraged unless absolutely necessary and validity is externally guaranteed.

#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)] // Add derive for printing and comparison
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

// Safer approach: Implement a conversion function
fn color_from_u8(value: u8) -> Option<Color> {
    match value {
        0 => Some(Color::Red),
        1 => Some(Color::Green),
        2 => Some(Color::Blue),
        _ => None, // Handle invalid values gracefully
    }
}

fn main() {
    let value: u8 = 1;
    let invalid_value: u8 = 5;

    // Safe conversion using our function
    match color_from_u8(value) {
        Some(color) => println!("Safe conversion ({}): Color is {:?}", value, color),
        None => println!("Safe conversion ({}): Invalid value", value),
    }
    match color_from_u8(invalid_value) {
        Some(color) => println!("Safe conv. ({}): Color is {:?}", invalid_value, color),
        None => println!("Safe conversion ({}): Invalid value", invalid_value),
    }

    // Unsafe conversion using transmute (Avoid this!)
    // Only do this if you are *certain* 'value' is valid.
    // If 'value' were 5, this would be Undefined Behavior.
    if value <= 2 { // Basic check before unsafe block
        let color_unsafe = unsafe { std::mem::transmute::<u8, Color>(value) };
        println!("Unsafe conversion ({}): Color is {:?}", value, color_unsafe);
    }
}
  • std::mem::transmute: Unsafe. Reinterprets bits. Using it for integer-to-enum casts where the integer might be invalid leads to Undefined Behavior.
  • Safe Alternatives: Implement a checked conversion function (like color_from_u8) returning Option or Result. This is the idiomatic and safe Rust approach. External crates like num_enum can automate creating such conversions.

10.2.5 Using Enum Discriminants for Array Indexing

If enum variants have sequential, non-negative discriminants starting from zero, they can be safely cast to usize for array indexing.

#[repr(usize)] // Use usize for direct indexing
#[derive(Debug, Clone, Copy, PartialEq, Eq)] // Derive traits needed
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

fn main() {
    let color_names = ["Red", "Green", "Blue"];
    let selected_color = Color::Green;

    // Cast the enum variant to usize to use as an index
    let index = selected_color as usize;

    // Bounds check is good practice, though guaranteed here by definition
    assert!(index < color_names.len());
    println!("Selected color name: {}", color_names[index]);

    // Direct access is safe if #[repr(usize)] and values match indices 0..N-1
    println!("Direct access: {}", color_names[Color::Blue as usize]);
}
  • Casting: Convert the variant to usize using as.
  • Safety: Ensure variants map directly to valid indices (0 to length-1). #[repr(usize)] and sequential definitions from 0 help guarantee this.

10.2.6 Advantages of Rust’s Simple Enums over C

Even basic Rust enums offer significant advantages:

  • Strong Type Safety: They are distinct types, not just integer aliases. Prevents accidental mixing of types.
  • Namespacing: Variants are typically namespaced by the enum type (Direction::North), avoiding name clashes common with C enums.
  • No Implicit Conversions: Conversions between enums and integers require explicit as casts, making intent clear.
  • Exhaustiveness Checking: match expressions require handling all variants, preventing bugs from forgotten cases.

10.2.7 Iterating and Sequencing Basic Enums

Coming from C, you might expect ways to easily iterate through all variants of a simple enum or find the “next” or “previous” variant based on its underlying integer value. Rust doesn’t provide this automatically because enums are treated primarily as distinct types, not just sequential integers. However, you can implement these capabilities when needed.

Iterating Over Variants

A common pattern to enable iteration is to define an associated constant slice containing all variants of the enum.

#[derive(Debug, PartialEq, Eq, Clone, Copy)] // Added traits
enum Direction {
    North,
    East,
    South,
    West,
}

impl Direction {
    // Define a constant array holding all variants in order
    const VARIANTS: [Direction; 4] = [
        Direction::North,
        Direction::East,
        Direction::South,
        Direction::West,
    ];
}

fn main() {
    println!("All directions:");
    // Iterate over the associated constant array
    for dir in Direction::VARIANTS.iter() {
        // '.iter()' borrows the elements, 'dir' is a &Direction
        print!("  Processing variant: {:?}", dir);
        // Example of using the variant in a match
        match dir {
            Direction::North => println!(" (It's North!)"),
            _ => println!(""), // Handle other variants minimally here
        }
    }
}

This manual approach works well for enums with a small, fixed number of variants. For more complex scenarios or to avoid maintaining the list manually, crates like strum or enum_iterator use procedural macros (e.g., #[derive(EnumIter)]) to generate this iteration logic automatically at compile time.

Finding the Next or Previous Variant

To implement sequencing (like getting the next direction in a cycle), you typically need to:

  1. Define explicit integer discriminants using #[repr(...)].
  2. Convert the current variant to its integer value.
  3. Perform arithmetic (e.g., add 1, using the modulo operator % for wrapping).
  4. Convert the resulting integer back into an enum variant safely, using a helper function.

Let’s add next() and prev() methods to our Direction enum:

#[repr(u8)] // Define underlying type for reliable casting
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum Direction {
    North = 0, // Assign explicit values starting from 0
    East  = 1,
    South = 2,
    West  = 3,
}

impl Direction {
    const COUNT: u8 = 4; // Number of variants

    // Function to safely convert from integer back to Direction
    // (Could also be implemented using crates like `num_enum`)
    fn from_u8(value: u8) -> Option<Direction> {
        match value {
            0 => Some(Direction::North),
            1 => Some(Direction::East),
            2 => Some(Direction::South),
            3 => Some(Direction::West),
            _ => None, // Return None for invalid values
        }
    }

    // Method to get the next direction (wrapping around)
    fn next(&self) -> Direction {
        let current_value = *self as u8; // Get integer value of the current variant
        let next_value = (current_value + 1) % Direction::COUNT; // next wrapping value
        // We know next_value will be valid (0..3) due to modulo COUNT,
        // so unwrap() is safe here. A production system might prefer
        // returning Option<Direction> or using a more robust from_u8.
        Direction::from_u8(next_value).expect("Logic error: next_value out of range")
    }

    // Method to get the previous direction (wrapping around)
    fn prev(&self) -> Direction {
        let current_value = *self as u8;
        // Add COUNT before subtracting 1 to handle unsigned wrapping correctly
        let prev_value = (current_value + Direction::COUNT - 1) % Direction::COUNT;
        // As above, we expect prev_value to be valid.
        Direction::from_u8(prev_value).expect("Logic error: prev_value out of range")
    }
}

fn main() {
    let mut heading = Direction::East;
    println!("Start: {:?}", heading); // East

    heading = heading.next();
    println!("Next:  {:?}", heading); // South

    heading = heading.prev();
    println!("Prev:  {:?}", heading); // East

    heading = heading.prev();
    println!("Prev:  {:?}", heading); // North (wraps)

    heading = heading.prev();
    println!("Prev:  {:?}", heading); // West (wraps)

    heading = heading.next();
    println!("Next:  {:?}", heading); // North
}
  • #[repr(u8)] and Explicit Values: Essential for predictable integer conversions starting from 0.
  • from_u8 Helper: Provides safe conversion back from the integer discriminant. Using expect() in next/prev relies on the modulo arithmetic correctly constraining values to the valid range 0..=3. If the logic were more complex or variants non-sequential, returning Option<Direction> would be safer.
  • Modulo Arithmetic: The % Direction::COUNT ensures wrapping behaviour (West -> North, North -> West). The + Direction::COUNT in prev ensures correct calculation with unsigned integers when current_value is 0.

These examples demonstrate how to add iteration and sequencing capabilities to basic Rust enums when required, bridging a potential gap for programmers accustomed to C’s treatment of enums as raw integers.