Skip to main content

Rust Enums Basics

Introduction

Enumerations, or "enums" for short, are one of Rust's most powerful features. An enum allows you to define a type by enumerating its possible variants. Think of enums as a way to say, "This value can be one of several different things, but only one at a time."

Unlike enums in many other languages which are often just sets of named constants, Rust's enums are much more flexible and powerful, forming the basis for many of Rust's unique patterns and capabilities.

What Are Enums?

In simplest terms, an enum is a type that can have a fixed set of values, called "variants." Each variant in the enum is distinct and can optionally contain data.

Let's start with a basic example:

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

fn main() {
let heading = Direction::North;

match heading {
Direction::North => println!("Heading north!"),
Direction::South => println!("Heading south!"),
Direction::East => println!("Heading east!"),
Direction::West => println!("Heading west!"),
}
}

Output:

Heading north!

In this example:

  • We define an enum named Direction with four variants
  • We create a variable heading of type Direction with the value Direction::North
  • We use a match statement to handle all possible variants

Defining Enums

The syntax for defining an enum is:

rust
enum EnumName {
Variant1,
Variant2,
// ...more variants
}

Rust convention is to use PascalCase for both the enum name and its variants.

Data in Enums

What makes Rust's enums particularly powerful is their ability to store data within each variant. Each variant can:

  1. Have no data (like our Direction example above)
  2. Have unnamed data (tuple-like)
  3. Have named fields (struct-like)

Let's see an example that combines all three:

rust
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields (struct-like)
Write(String), // Single value (tuple-like)
ChangeColor(i32, i32, i32), // Multiple values (tuple-like)
}

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

for msg in messages {
process_message(msg);
}
}

fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quitting the application"),
Message::Move { x, y } => println!("Moving to position: ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Changing color to RGB: ({}, {}, {})", r, g, b),
}
}

Output:

Quitting the application
Moving to position: (10, 5)
Text message: Hello, Rust!
Changing color to RGB: (255, 0, 255)

This flexibility allows enums to model a wide variety of scenarios in a type-safe way.

The Option Enum

One of the most important enums in Rust's standard library is Option<T>. This enum is so commonly used that it's included in the prelude (automatically imported).

rust
enum Option<T> {
None,
Some(T),
}

Option<T> represents a value that might be present (Some(T)) or absent (None). It's Rust's way of handling the concept of nullable values without actually having null in the language.

Let's see it in action:

rust
fn main() {
let some_number = Some(5);
let some_string = Some("a string");

// To declare a None value, we need to specify the type, since
// the compiler can't infer what type of Option<T> it is
let absent_number: Option<i32> = None;

// Using Option with pattern matching
match some_number {
Some(num) => println!("The number is: {}", num),
None => println!("There is no number"),
}

// Using if let for simpler matching when you only care about one variant
if let Some(text) = some_string {
println!("The string is: {}", text);
}

// Using unwrap_or to provide a default value
let value = absent_number.unwrap_or(0);
println!("Value with default: {}", value);
}

Output:

The number is: 5
The string is: a string
Value with default: 0

The Match Control Flow Operator

As you've seen in the examples, the match operator is particularly useful with enums. It ensures that you handle all possible cases, making your code more robust.

Here's a more complete example using a weather forecast enum:

rust
enum Weather {
Sunny,
Cloudy,
Rainy(f32), // Amount of rainfall in inches
Snowy(f32), // Amount of snowfall in inches
}

fn get_activity(weather: Weather) -> String {
match weather {
Weather::Sunny => String::from("Go for a hike!"),
Weather::Cloudy => String::from("Perhaps read a book outside."),
Weather::Rainy(amount) if amount < 1.0 => String::from("A light walk with an umbrella."),
Weather::Rainy(_) => String::from("Stay inside and watch a movie."),
Weather::Snowy(amount) if amount < 2.0 => String::from("Build a small snowman."),
Weather::Snowy(_) => String::from("Best to stay warm indoors."),
}
}

fn main() {
let forecasts = [
Weather::Sunny,
Weather::Cloudy,
Weather::Rainy(0.5),
Weather::Rainy(2.3),
Weather::Snowy(1.0),
Weather::Snowy(5.0),
];

for forecast in forecasts {
let activity = get_activity(forecast);
println!("Suggested activity: {}", activity);
}
}

Output:

Suggested activity: Go for a hike!
Suggested activity: Perhaps read a book outside.
Suggested activity: A light walk with an umbrella.
Suggested activity: Stay inside and watch a movie.
Suggested activity: Build a small snowman.
Suggested activity: Best to stay warm indoors.

Notice how we use guards (if amount < 1.0) to further refine our pattern matching.

Enum Methods

Like structs, enums can have methods implemented using the impl block:

rust
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64), // three sides
}

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) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}

fn describe(&self) -> String {
match self {
Shape::Circle(_) => String::from("a circle"),
Shape::Rectangle(_, _) => String::from("a rectangle"),
Shape::Triangle(_, _, _) => String::from("a triangle"),
}
}
}

fn main() {
let shapes = [
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle(3.0, 4.0, 5.0),
];

for shape in shapes {
println!("The area of {} is {:.2}", shape.describe(), shape.area());
}
}

Output:

The area of a circle is 78.54
The area of a rectangle is 24.00
The area of a triangle is 6.00

Real-World Applications

Enums are extremely useful in many real-world scenarios. Here are a few common use cases:

1. Representing Results with Error Handling

Rust's standard library includes the Result<T, E> enum for handling operations that might fail:

rust
enum Result<T, E> {
Ok(T), // Success with value T
Err(E), // Error with value E
}

Here's how you might use it:

rust
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}

fn main() {
let results = [
divide(10, 2),
divide(5, 0),
];

for result in results {
match result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
}
}

Output:

Result: 5
Error: Cannot divide by zero

2. State Machines

Enums are perfect for representing states in a state machine:

rust
enum ConnectionState {
Disconnected,
Connecting { retries: u32 },
Connected { server_id: String },
Disconnecting,
}

struct Connection {
state: ConnectionState,
}

impl Connection {
fn new() -> Self {
Self { state: ConnectionState::Disconnected }
}

fn connect(&mut self, server: &str) {
match self.state {
ConnectionState::Disconnected => {
println!("Initiating connection to {}", server);
self.state = ConnectionState::Connecting { retries: 0 };
}
ConnectionState::Connecting { retries } if retries < 3 => {
println!("Retry {} connecting to {}", retries + 1, server);
self.state = ConnectionState::Connecting { retries: retries + 1 };
}
ConnectionState::Connecting { .. } => {
println!("Connection successful to {}", server);
self.state = ConnectionState::Connected { server_id: server.to_string() };
}
_ => println!("Cannot connect in current state")
}
}

fn status(&self) -> String {
match &self.state {
ConnectionState::Disconnected => "Disconnected".to_string(),
ConnectionState::Connecting { retries } => format!("Connecting (attempt {})", retries + 1),
ConnectionState::Connected { server_id } => format!("Connected to {}", server_id),
ConnectionState::Disconnecting => "Disconnecting".to_string(),
}
}
}

fn main() {
let mut conn = Connection::new();
println!("Status: {}", conn.status());

conn.connect("db.example.com");
println!("Status: {}", conn.status());

conn.connect("db.example.com");
println!("Status: {}", conn.status());

conn.connect("db.example.com");
println!("Status: {}", conn.status());

conn.connect("db.example.com");
println!("Status: {}", conn.status());
}

Output:

Status: Disconnected
Initiating connection to db.example.com
Status: Connecting (attempt 1)
Retry 1 connecting to db.example.com
Status: Connecting (attempt 2)
Retry 2 connecting to db.example.com
Status: Connecting (attempt 3)
Connection successful to db.example.com
Status: Connected to db.example.com

3. Typed Configuration

Enums can represent different configuration options in a type-safe way:

rust
enum DatabaseConfig {
MySQL {
host: String,
port: u16,
username: String,
password: String,
database_name: String,
},
PostgreSQL {
connection_string: String,
},
SQLite {
file_path: String,
},
}

fn connect_to_database(config: DatabaseConfig) {
match config {
DatabaseConfig::MySQL { host, port, username, database_name, .. } => {
println!("Connecting to MySQL database '{}' on {}:{} as user '{}'",
database_name, host, port, username);
// MySQL-specific connection code would go here
},
DatabaseConfig::PostgreSQL { connection_string } => {
println!("Connecting to PostgreSQL with connection string: {}", connection_string);
// PostgreSQL-specific connection code would go here
},
DatabaseConfig::SQLite { file_path } => {
println!("Opening SQLite database at: {}", file_path);
// SQLite-specific connection code would go here
},
}
}

fn main() {
let configs = [
DatabaseConfig::MySQL {
host: "localhost".to_string(),
port: 3306,
username: "admin".to_string(),
password: "password123".to_string(),
database_name: "users".to_string(),
},
DatabaseConfig::PostgreSQL {
connection_string: "postgresql://user:pass@localhost/mydb".to_string(),
},
DatabaseConfig::SQLite {
file_path: "/var/data/app.db".to_string(),
},
];

for config in configs {
connect_to_database(config);
}
}

Output:

Connecting to MySQL database 'users' on localhost:3306 as user 'admin'
Connecting to PostgreSQL with connection string: postgresql://user:pass@localhost/mydb
Opening SQLite database at: /var/data/app.db

Visual Representation of Enums

Here's a diagram to help visualize how Rust enums work:

Summary

Rust's enums are incredibly powerful and flexible. They allow you to:

  • Define a type that can have different variants
  • Store different types and amounts of data in each variant
  • Use pattern matching to handle all possible cases
  • Build type-safe code that the compiler can verify

Enums form the backbone of many idiomatic Rust patterns, including:

  • The Option<T> type for handling optional values without null
  • The Result<T, E> type for error handling
  • State machines and complex data modeling
  • Protocol and message definitions

By mastering enums, you're well on your way to writing Rust code that is both expressive and robust.

Exercises

  1. Basic Enum: Create an enum TrafficLight with variants Red, Yellow, and Green. Write a function that takes a traffic light and returns how long a driver should wait (e.g., stop for red, prepare to stop for yellow, go for green).

  2. Enum with Data: Define an enum Payment with variants for different payment methods (e.g., Cash, CreditCard with card number and expiry, PayPal with email). Write a function to process payments that handles each method differently.

  3. Recursive Enum: Try implementing a simple binary tree structure using enums where each node is either a leaf or contains a value and two child nodes.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)