On this page
Enums and pattern matching
Enums: representing multiple shapes
Enums (enumerations) in Rust are far more powerful than in most languages. Each variant can carry different data, making Rust's enums equivalent to "sum types" or "union types" from type theory.
Basic enums
#[derive(Debug)]
enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
fn is_workday(day: &DayOfWeek) -> bool {
matches!(day, DayOfWeek::Monday | DayOfWeek::Tuesday | DayOfWeek::Wednesday
| DayOfWeek::Thursday | DayOfWeek::Friday)
}
fn main() {
let today = DayOfWeek::Wednesday;
println!("Today: {:?}", today);
println!("Is workday? {}", is_workday(&today));
}Enums with data
The most powerful feature: each variant can carry different types of data:
#[derive(Debug)]
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Anonymous struct
Write(String), // A String
Color(u8, u8, u8), // Three u8 (RGB)
Attachment { name: String, bytes: Vec<u8> }, // Struct with Vec
}
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("Shutting down..."),
Message::Move { x, y } => println!("Moving to ({x}, {y})"),
Message::Write(t) => println!("Text received: {t}"),
Message::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
Message::Attachment { name, bytes } => {
println!("Attachment '{}' ({} bytes)", name, bytes.len())
}
}
}
}
fn main() {
let messages = vec![
Message::Move { x: 10, y: 20 },
Message::Write(String::from("Hello!")),
Message::Color(255, 128, 0),
Message::Attachment {
name: String::from("photo.jpg"),
bytes: vec![0xFF, 0xD8, 0xFF],
},
Message::Quit,
];
for m in &messages {
m.process();
}
}Option: goodbye to null
Rust has no null. Instead, it uses the Option<T> enum from the standard library:
enum Option<T> {
Some(T), // There is a value
None, // There is no value
}fn find_index(list: &[i32], target: i32) -> Option<usize> {
for (i, &n) in list.iter().enumerate() {
if n == target {
return Some(i);
}
}
None
}
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
match find_index(&numbers, 5) {
Some(i) => println!("Found at index {i}"),
None => println!("Not found"),
}
// Convenient Option methods
let result = divide(10.0, 3.0);
println!("Result: {:.4}", result.unwrap_or(0.0));
let nothing = divide(5.0, 0.0);
println!("Division by zero: {}", nothing.is_none());
// map: transform the value if it exists
let doubled = result.map(|r| r * 2.0);
println!("Doubled: {:.4}", doubled.unwrap_or(0.0));
// and_then: chain operations that can fail
let text = Some("42");
let number: Option<i32> = text.and_then(|s| s.parse().ok());
println!("Number: {:?}", number);
}Result: error handling
Result<T, E> is the other fundamental enum from the standard library:
enum Result<T, E> {
Ok(T), // Successful operation with value T
Err(E), // Error of type E
}use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse::<i32>()
}
fn main() {
let cases = ["42", " -7 ", "hello", "999999999999"];
for case in &cases {
match parse_number(case) {
Ok(n) => println!("'{case}' → {n}"),
Err(e) => println!("'{case}' → Error: {e}"),
}
}
}match: the heart of pattern matching
match is Rust's most powerful operator for working with enums. It is exhaustive: it must cover all possible cases:
fn describe_number(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=9 => "digit",
10..=99 => "two digits",
100..=999 => "three digits",
_ => "large",
}
}
fn classify_char(c: char) -> &'static str {
match c {
'a'..='z' | 'á' | 'é' | 'í' | 'ó' | 'ú' => "lowercase letter",
'A'..='Z' | 'Á' | 'É' | 'Í' | 'Ó' | 'Ú' => "uppercase letter",
'0'..='9' => "digit",
' ' | '\t' | '\n' => "whitespace",
_ => "symbol",
}
}
fn main() {
for n in [-5, 0, 7, 42, 500, 10_000] {
println!("{n}: {}", describe_number(n));
}
for c in ['a', 'Z', '5', ' ', '@'] {
println!("'{c}': {}", classify_char(c));
}
}Guards in match
Guards allow adding extra conditions to match arms:
fn evaluate_temperature(temp: f64) -> &'static str {
match temp {
t if t < -30.0 => "dangerously cold",
t if t < 0.0 => "below freezing",
t if t < 15.0 => "cold",
t if t < 25.0 => "comfortable",
t if t < 35.0 => "warm",
t if t < 40.0 => "very hot",
_ => "dangerously hot",
}
}
fn main() {
for temp in [-40.0, -5.0, 10.0, 22.0, 33.0, 38.0, 42.0] {
println!("{temp:.1}°C: {}", evaluate_temperature(temp));
}
}if let and while let: concise pattern matching
When you only care about one case of the enum:
fn main() {
let config: Option<u32> = Some(8080);
// With match (verbose for a single case):
match config {
Some(port) => println!("Configured port: {port}"),
None => (), // Do nothing
}
// With if let (more concise):
if let Some(port) = config {
println!("Port: {port}");
}
// if let with else:
if let Some(port) = config {
println!("Custom port: {port}");
} else {
println!("Using default port: 3000");
}
// while let: iterate until the pattern no longer matches
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top) = stack.pop() {
print!("{top} ");
}
println!();
// Chaining if let
let value: Result<Option<i32>, String> = Ok(Some(42));
if let Ok(Some(n)) = value {
println!("Nested value: {n}");
}
}Nested patterns and destructuring
#[derive(Debug)]
struct Point { x: i32, y: i32 }
fn main() {
let point = Point { x: 3, y: -5 };
// Destructuring in match
let description = match point {
Point { x: 0, y: 0 } => "origin",
Point { x, y: 0 } => "on the X axis",
Point { x: 0, y } => "on the Y axis",
Point { x, y } if x == y => "on the diagonal",
_ => "arbitrary point",
};
println!("{description}");
// Binding with @
let n = 15;
match n {
x @ 1..=10 => println!("{x} is between 1 and 10"),
x @ 11..=20 => println!("{x} is between 11 and 20"),
x => println!("{x} is out of range"),
}
// Tuples in match
let coordinates = (1, -1);
match coordinates {
(0, 0) => println!("Origin"),
(x, 0) | (0, x) => println!("On axis with {x}"),
(x, y) if x == -y => println!("Anti-diagonal"),
(x, y) => println!("({x}, {y})"),
}
}Enums and pattern matching make Rust code extraordinarily expressive. The next lesson covers collections from the standard library — Vec, String, and HashMap.
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle { width: f64, height: f64 },
Triangle(f64, f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
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 name(&self) -> &str {
match self {
Shape::Circle(_) => "Circle",
Shape::Rectangle { .. } => "Rectangle",
Shape::Triangle(..) => "Triangle",
}
}
}
fn main() {
let shapes: Vec<Shape> = vec![
Shape::Circle(5.0),
Shape::Rectangle { width: 4.0, height: 6.0 },
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{}: area = {:.4}", shape.name(), shape.area());
}
}
Sign in to track your progress