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
, andDirection::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 theDirection
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
) returningOption
orResult
. This is the idiomatic and safe Rust approach. External crates likenum_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
usingas
. - 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:
- Define explicit integer discriminants using
#[repr(...)]
. - Convert the current variant to its integer value.
- Perform arithmetic (e.g., add 1, using the modulo operator
%
for wrapping). - 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. Usingexpect()
innext
/prev
relies on the modulo arithmetic correctly constraining values to the valid range0..=3
. If the logic were more complex or variants non-sequential, returningOption<Direction>
would be safer.- Modulo Arithmetic: The
% Direction::COUNT
ensures wrapping behaviour (West -> North, North -> West). The+ Direction::COUNT
inprev
ensures correct calculation with unsigned integers whencurrent_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.