Skip to content

Rust - Enums

Enumerations, or enums, are a core feature in Rust that allow you to define a type by listing its possible variants. When you have a value that can only be one of a specific set of possibilities, an enum is the perfect tool.

enum WebEvent {
// A unit-like variant
PageLoad,
// A tuple-struct variant
KeyPress(char),
// A struct-like variant
Click { x: i64, y: i64 },
}

Here, WebEvent is a single type that can be one of three distinct variants. This approach is more concise and type-safe than defining multiple separate structs.

Let’s create instances of the WebEvent variants. To make our custom type printable for debugging, we can add the #[derive(Debug)] attribute. This automatically implements the Debug trait, allowing us to use the {:?} format specifier in println!.

#[derive(Debug)]
enum WebEvent {
PageLoad,
KeyPress(char),
Click { x: i64, y: i64 },
}
fn main() {
let load = WebEvent::PageLoad;
let key = WebEvent::KeyPress('k');
let click = WebEvent::Click { x: 20, y: 80 };
println!("{:?}", load);
println!("{:?}", key);
println!("{:?}", click);
}
PageLoad
KeyPress('k')
Click { x: 20, y: 80 }

Rust has a powerful control flow expression, match, that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Enums and match are a perfect combination.

A key advantage of match is that it’s exhaustive. The compiler checks that you’ve covered every possible variant of the enum. If you forget one, your code won’t compile, preventing a whole class of bugs.

fn inspect_event(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("Page loaded"),
WebEvent::KeyPress(c) => println!("Pressed '{}'.", c),
WebEvent::Click { x, y } => {
println!("Clicked at x={}, y={}.", x, y);
}
}
}
fn main() {
let load = WebEvent::PageLoad;
let key = WebEvent::KeyPress('k');
let click = WebEvent::Click { x: 20, y: 80 };
inspect_event(load);
inspect_event(key);
inspect_event(click);
}
Page loaded
Pressed 'k'.
Clicked at x=20, y=80.

Rust doesn’t have null. Instead, it has a standard library enum, Option<T>, that encodes the concept of a value being present or absent. This is a huge benefit, as it forces you to handle the ‘null’ case at compile time, preventing a wide class of bugs.

enum Option<T> {
Some(T), // Represents the presence of a value of type T
None, // Represents the absence of a value
}

Let’s see it in action with a function that might not return a value. Because Option<T> is an enum, the compiler ensures you handle the None case, making your code more robust.

fn find_user(name: &str) -> Option<u32> {
match name {
"Alice" => Some(101),
"Bob" => Some(102),
_ => None,
}
}
fn main() {
let user_id = find_user("Alice");
match user_id {
Some(id) => println!("Found user ID: {}", id),
None => println!("User not found."),
}
}
Found user ID: 101

Sometimes, a match expression is too verbose if you only care about one specific variant. For these cases, Rust provides if let and while let as more concise syntax.

fn main() {
let maybe_number: Option<i32> = Some(7);
// Using `if let` is more concise than a `match` with a single arm.
if let Some(number) = maybe_number {
println!("The number is: {}", number);
} else {
println!("There is no number.");
}
// `while let` loops as long as the pattern matches.
let mut optional = Some(0);
while let Some(i) = optional {
if i > 5 {
println!("Greater than 5, quitting.");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
}
}

if let Some(number) = maybe_number can be read as: “if maybe_number is a Some value, bind its inner value to the number variable for the if block.”

  • Embrace match and if let: Always use these constructs to handle Option and other enums. Avoid .unwrap() or .expect(), which can cause your program to panic (crash). Use .expect("message") only when you are absolutely certain a value will be Some and a None represents a critical, unrecoverable bug.
  • Use Enums to Model State: Enums are excellent for creating state machines. For example, enum PageState { Loading, Loaded(Content), Error(String) }. This makes invalid states unrepresentable in your code.
  • Keep Variants Cohesive: The variants of an enum should represent different states of a single, unified concept.

Similar to Option, the Result<T, E> enum is the standard way to handle operations that can succeed or fail. It is defined as enum Result<T, E> { Ok(T), Err(E) }. You’ll encounter it everywhere in Rust, especially in I/O and parsing operations. We’ll cover it in more detail in the error handling chapter.