Skip to main content

Understanding Rust Enums and Pattern Matching

Enums in Rust

Enums (enumerations) in Rust are a way to define a type that can be one of several possible variants. Unlike enums in some other languages that are limited to a list of named constants, Rust's enums are much more powerful and flexible 1.

Basic Enum Definition

Here's how you define a simple enum:

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

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

Enums with Associated Data

One of Rust's most powerful enum features is that variants can hold different types and amounts of data:

enum WebEvent {
// Unit-like variants (no data)
PageLoad,
PageUnload,

// Tuple-like variants (with data)
KeyPress(char),
Paste(String),

// Struct-like variants
Click { x: i64, y: i64 },
}

fn main() {
let key_event = WebEvent::KeyPress('x');
let paste_event = WebEvent::Paste(String::from("Hello, Rust!"));
let click_event = WebEvent::Click { x: 20, y: 80 };
}

This flexibility allows enums to represent complex domain models where a value might be one of several possible types 2.

Example: IP Address Enum

Here's an example showing how an enum can represent different types of IP addresses:

enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}

This example shows how different variants can contain different types and amounts of data 1.

Pattern Matching with match

Pattern matching is a powerful feature in Rust that works particularly well with enums. The match expression allows you to compare a value against a series of patterns and execute code based on which pattern matches.

Basic Pattern Matching

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
println!("A quarter is worth {} cents", value_in_cents(Coin::Quarter));
}

Pattern Matching with Data Extraction

When an enum variant holds data, you can extract that data in the match pattern:

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

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);
},
}
}

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

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

The _ Placeholder and if Guards

For more complex pattern matching, you can use the _ placeholder to match any value and if guards to add conditions:

fn main() {
let num = 13;

match num {
// Match a specific value
1 => println!("One!"),

// Match with a guard condition
n if n % 2 == 0 => println!("Even number: {}", n),

// Match a range of values
2..=10 => println!("Between 2 and 10: {}", num),

// Default case
_ => println!("Something else: {}", num),
}
}

Pattern Matching with Option<T>

Option<T> is a built-in enum in Rust that represents a value that may or may not exist:

fn main() {
let some_number = Some(5);
let absent_number: Option<i32> = None;

match some_number {
Some(n) => println!("Got a number: {}", n),
None => println!("No number found"),
}

match absent_number {
Some(n) => println!("Got a number: {}", n),
None => println!("No number found"),
}
}

Advanced Pattern Matching Features

if let for Concise Pattern Matching

For cases when you only care about one pattern, if let provides a more concise syntax:

fn main() {
let some_value = Some(3);

// Using match
match some_value {
Some(3) => println!("Found a three!"),
_ => (), // Do nothing for other cases
}

// Using if let (more concise)
if let Some(3) = some_value {
println!("Found a three!");
}
}

while let for Conditional Loops

You can use while let to continue a loop as long as a pattern matches:

fn main() {
let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

// Pop values from the stack while it's not empty
while let Some(value) = stack.pop() {
println!("Popped: {}", value);
}
}

Type Aliases for Enums

When enum names are long, you can create type aliases for them:

enum VeryVerboseEnumOfThingsToDoWithNumbers {
Add,
Subtract,
Multiply,
Divide,
}

// Create a type alias
type Operation = VeryVerboseEnumOfThingsToDoWithNumbers;

fn main() {
// Now we can use the shorter name
let op = Operation::Add;

match op {
Operation::Add => println!("Addition"),
Operation::Subtract => println!("Subtraction"),
Operation::Multiply => println!("Multiplication"),
Operation::Divide => println!("Division"),
}
}

This can make your code more readable when dealing with complex enums 2.

Rust's enums and pattern matching system provide a powerful and type-safe way to handle different possibilities in your code, making it safer and more expressive compared to many other programming languages.