Enums & Pattern Matching
Enums (enumerations) allow you to define types that can be one of several variants. Combined with Rust's powerful pattern matching, enums enable expressive and safe code that handles different states and conditions elegantly.
Basic Enum Definition
Enums define a type with multiple possible variants:
enum Direction {
North,
South,
East,
West,
}
fn main() {
let direction = Direction::North;
match direction {
Direction::North => println!("Going north!"),
Direction::South => println!("Going south!"),
Direction::East => println!("Going east!"),
Direction::West => println!("Going west!"),
}
}
Enums with Data
Enum variants can contain data:
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields (like a struct)
Write(String), // Single field
ChangeColor(i32, i32, i32), // Multiple fields (like a tuple)
}
fn process_message(message: Message) {
match message {
Message::Quit => println!("Quitting..."),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => println!("Changing color to RGB({}, {}, {})", r, g, b),
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 20 },
Message::Write(String::from("Hello, world!")),
Message::ChangeColor(255, 0, 0),
];
for message in messages {
process_message(message);
}
}
Pattern Matching with Match
The match
expression is Rust's primary pattern matching construct:
Basic Match Patterns
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 describe_coin(coin: Coin) -> String {
match coin {
Coin::Penny => {
println!("Lucky penny!");
String::from("Penny")
}
Coin::Nickel => String::from("Nickel"),
Coin::Dime => String::from("Dime"),
Coin::Quarter => String::from("Quarter"),
}
}
Matching with Guards
Add conditions to match arms with guards:
fn categorize_number(x: i32) -> &'static str {
match x {
n if n < 0 => "negative",
0 => "zero",
n if n > 0 && n <= 10 => "small positive",
n if n > 10 && n <= 100 => "medium positive",
_ => "large positive",
}
}
fn main() {
let numbers = vec![-5, 0, 3, 15, 150];
for num in numbers {
println!("{} is {}", num, categorize_number(num));
}
}
Matching Multiple Patterns
fn is_weekend(day: &str) -> bool {
match day {
"Saturday" | "Sunday" => true,
_ => false,
}
}
fn categorize_character(c: char) -> &'static str {
match c {
'a'..='z' => "lowercase letter",
'A'..='Z' => "uppercase letter",
'0'..='9' => "digit",
' ' | '\t' | '\n' | '\r' => "whitespace",
_ => "other",
}
}
Advanced Enum Patterns
Recursive Enums
Enums can contain themselves (with indirection):
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
impl<T> List<T> {
fn new() -> Self {
List::Nil
}
fn prepend(self, elem: T) -> Self {
List::Cons(elem, Box::new(self))
}
fn len(&self) -> usize {
match self {
List::Cons(_, tail) => 1 + tail.len(),
List::Nil => 0,
}
}
}
fn main() {
let list = List::new()
.prepend(1)
.prepend(2)
.prepend(3);
println!("List: {:?}", list);
println!("Length: {}", list.len());
}
Generic Enums
#[derive(Debug)]
enum Result<T, E> {
Ok(T),
Err(E),
}
#[derive(Debug)]
enum Option<T> {
Some(T),
None,
}
// Custom generic enum
#[derive(Debug)]
enum Either<L, R> {
Left(L),
Right(R),
}
impl<L, R> Either<L, R> {
fn is_left(&self) -> bool {
matches!(self, Either::Left(_))
}
fn is_right(&self) -> bool {
matches!(self, Either::Right(_))
}
}
fn main() {
let left: Either<i32, String> = Either::Left(42);
let right: Either<i32, String> = Either::Right("hello".to_string());
println!("Left is left: {}", left.is_left());
println!("Right is right: {}", right.is_right());
}
Working with Option<T>
Option<T>
represents a value that might be present or absent:
Basic Option Usage
fn find_word(text: &str, word: &str) -> Option<usize> {
text.find(word)
}
fn main() {
let text = "Hello, world!";
match find_word(text, "world") {
Some(index) => println!("Found 'world' at index: {}", index),
None => println!("'world' not found"),
}
// Using if let for simpler cases
if let Some(index) = find_word(text, "Hello") {
println!("Found 'Hello' at index: {}", index);
}
}
Option Methods
fn option_methods_example() {
let some_number = Some(5);
let no_number: Option<i32> = None;
// unwrap_or: provide default value
println!("Value or default: {}", some_number.unwrap_or(0));
println!("Value or default: {}", no_number.unwrap_or(0));
// map: transform the contained value
let doubled = some_number.map(|x| x * 2);
println!("Doubled: {:?}", doubled);
// and_then: chain operations that return Option
let result = some_number.and_then(|x| {
if x > 0 {
Some(x * 2)
} else {
None
}
});
println!("Chained result: {:?}", result);
// filter: keep value only if predicate is true
let filtered = some_number.filter(|&x| x > 3);
println!("Filtered: {:?}", filtered);
}
Option Pattern Matching
fn process_optional_data(data: Option<&str>) {
match data {
Some("") => println!("Got empty string"),
Some(text) if text.len() > 10 => println!("Got long text: {}", text),
Some(text) => println!("Got text: {}", text),
None => println!("No data provided"),
}
}
fn main() {
process_optional_data(Some("Hello"));
process_optional_data(Some(""));
process_optional_data(Some("This is a very long string"));
process_optional_data(None);
}
Working with Result<T, E>
Result<T, E>
represents operations that can succeed or fail:
Basic Result Usage
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
if b == 0.0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
fn main() {
// Pattern matching with Result
match parse_number("42") {
Ok(num) => println!("Parsed number: {}", num),
Err(e) => println!("Parse error: {}", e),
}
// Using if let for success case only
if let Ok(result) = divide(10.0, 2.0) {
println!("Division result: {}", result);
}
// Handling error case
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}
Result Methods
fn result_methods_example() -> Result<i32, &'static str> {
let good_result: Result<i32, &str> = Ok(10);
let bad_result: Result<i32, &str> = Err("something went wrong");
// unwrap_or: provide default value for error case
println!("Good result or default: {}", good_result.unwrap_or(0));
println!("Bad result or default: {}", bad_result.unwrap_or(0));
// map: transform success value
let doubled = good_result.map(|x| x * 2);
println!("Doubled: {:?}", doubled);
// map_err: transform error value
let mapped_error = bad_result.map_err(|e| format!("Error: {}", e));
println!("Mapped error: {:?}", mapped_error);
// and_then: chain operations that return Result
let chained = good_result.and_then(|x| {
if x > 5 {
Ok(x * 2)
} else {
Err("too small")
}
});
println!("Chained: {:?}", chained);
Ok(42)
}
The ? Operator
The ?
operator provides a concise way to propagate errors:
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?; // Propagate error if file doesn't open
let mut content = String::new();
file.read_to_string(&mut content)?; // Propagate error if read fails
Ok(content)
}
fn calculate_sum(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let num_a = a.parse::<i32>()?;
let num_b = b.parse::<i32>()?;
Ok(num_a + num_b)
}
fn main() {
match calculate_sum("10", "20") {
Ok(sum) => println!("Sum: {}", sum),
Err(e) => println!("Parse error: {}", e),
}
match calculate_sum("10", "abc") {
Ok(sum) => println!("Sum: {}", sum),
Err(e) => println!("Parse error: {}", e),
}
}
Advanced Pattern Matching
Destructuring Complex Patterns
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point },
Triangle(Point, Point, Point),
}
fn describe_shape(shape: Shape) {
match shape {
Shape::Circle { center: Point { x, y }, radius } => {
println!("Circle at ({}, {}) with radius {}", x, y, radius);
}
Shape::Rectangle {
top_left: Point { x: x1, y: y1 },
bottom_right: Point { x: x2, y: y2 }
} => {
println!("Rectangle from ({}, {}) to ({}, {})", x1, y1, x2, y2);
}
Shape::Triangle(p1, p2, p3) => {
println!("Triangle with points ({}, {}), ({}, {}), ({}, {})",
p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
}
}
Nested Pattern Matching
enum Message {
Text(String),
Number(i32),
Nested(Box<Message>),
Batch(Vec<Message>),
}
fn process_message(message: Message) {
match message {
Message::Text(text) if text.len() > 10 => {
println!("Long text: {}", text);
}
Message::Text(text) => {
println!("Short text: {}", text);
}
Message::Number(n) if n < 0 => {
println!("Negative number: {}", n);
}
Message::Number(n) => {
println!("Positive number: {}", n);
}
Message::Nested(boxed_msg) => {
println!("Processing nested message:");
process_message(*boxed_msg);
}
Message::Batch(messages) => {
println!("Processing batch of {} messages:", messages.len());
for msg in messages {
process_message(msg);
}
}
}
}
Pattern Matching with References
fn match_references() {
let numbers = vec![1, 2, 3, 4, 5];
for number in &numbers {
match number {
1 => println!("One!"),
2 | 3 => println!("Two or three"),
4..=10 => println!("Between four and ten"),
_ => println!("Something else"),
}
}
// Pattern matching with dereferencing
let option_number = Some(42);
match &option_number {
Some(&n) if n > 40 => println!("Big number: {}", n),
Some(&n) => println!("Small number: {}", n),
None => println!("No number"),
}
}
Useful Pattern Matching Techniques
Using matches!
Macro
enum Status {
Active,
Inactive,
Pending,
}
fn main() {
let status = Status::Active;
// Instead of match
let is_active = match status {
Status::Active => true,
_ => false,
};
// Use matches! macro
let is_active = matches!(status, Status::Active);
println!("Is active: {}", is_active);
// More complex patterns
let numbers = vec![1, 2, 3, 4, 5];
let count = numbers.iter().filter(|&&x| matches!(x, 1 | 2 | 3)).count();
println!("Count of 1, 2, or 3: {}", count);
}
Ignoring Values with _
enum Color {
Rgb(u8, u8, u8),
Hsv(u16, u8, u8),
}
fn process_color(color: Color) {
match color {
Color::Rgb(r, _, _) => println!("Red component: {}", r),
Color::Hsv(h, _, _) => println!("Hue: {}", h),
}
}
fn ignore_patterns() {
let point = (3, 5, 7);
match point {
(x, ..) => println!("x coordinate: {}", x), // Ignore remaining fields
}
let numbers = (1, 2, 3, 4, 5);
match numbers {
(first, .., last) => println!("First: {}, Last: {}", first, last),
}
}
Variable Binding with @
fn binding_examples() {
let number = 42;
match number {
n @ 1..=50 => println!("Small number: {}", n),
n @ 51..=100 => println!("Medium number: {}", n),
n => println!("Large number: {}", n),
}
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_var @ 3..=7 } => {
println!("Found id in range: {}", id_var);
}
Message::Hello { id } => {
println!("Found other id: {}", id);
}
}
}
Best Practices
1. Use Exhaustive Pattern Matching
enum State {
Loading,
Success(String),
Error(String),
}
// Good: Handle all cases
fn handle_state(state: State) {
match state {
State::Loading => println!("Loading..."),
State::Success(data) => println!("Success: {}", data),
State::Error(err) => println!("Error: {}", err),
}
}
// Avoid: Using _ when you should handle specific cases
fn handle_state_bad(state: State) {
match state {
State::Success(data) => println!("Success: {}", data),
_ => println!("Not success"), // Loses information
}
}
2. Use if let
for Simple Cases
// Good for simple Option/Result handling
fn simple_option_handling(opt: Option<i32>) {
if let Some(value) = opt {
println!("Got value: {}", value);
}
}
// Use match for complex cases
fn complex_option_handling(opt: Option<i32>) {
match opt {
Some(n) if n > 0 => println!("Positive: {}", n),
Some(n) if n < 0 => println!("Negative: {}", n),
Some(0) => println!("Zero"),
None => println!("No value"),
}
}
3. Prefer Descriptive Enum Names
// Good: Clear intent
enum PaymentStatus {
Pending,
Processing,
Completed,
Failed { reason: String },
}
// Avoid: Unclear meaning
enum Status {
A,
B,
C,
D(String),
}
4. Use Methods on Enums
enum ConnectionState {
Disconnected,
Connecting,
Connected { address: String },
Error { message: String },
}
impl ConnectionState {
fn is_connected(&self) -> bool {
matches!(self, ConnectionState::Connected { .. })
}
fn address(&self) -> Option<&str> {
match self {
ConnectionState::Connected { address } => Some(address),
_ => None,
}
}
}
Enums and pattern matching are fundamental to idiomatic Rust programming. They enable safe handling of different states and conditions while maintaining code clarity and preventing common bugs. Master these concepts to write more robust and expressive Rust code.