Series: Learn Rust from Scratch | Post 6 of 11
Introduction
Rust's enum is far more powerful than enums in most languages. In Rust, each variant of an enum can hold data of different types — making enums extremely expressive.
Paired with the match expression, enums become one of the most elegant tools in Rust. And two special enums — Option<T> and Result<T, E> — are central to how Rust handles the absence of values and errors safely.
Defining a Basic Enum
// Define an enum with variants
enum Direction {
North,
South,
East,
West,
}
fn describe_direction(dir: Direction) {
match dir {
Direction::North => println!("Heading North ↑"),
Direction::South => println!("Heading South ↓"),
Direction::East => println!("Heading East →"),
Direction::West => println!("Heading West ←"),
}
}
fn main() {
let my_direction = Direction::North;
describe_direction(my_direction);
describe_direction(Direction::East);
}
Output:
Heading North ↑
Heading East →
Enums with Data
Each variant can carry different types and amounts of data — this is what makes Rust enums special:
#[derive(Debug)]
enum Shape {
Circle(f64), // One f64 (radius)
Rectangle(f64, f64), // Two f64s (width, height)
Triangle(f64, f64, f64), // Three f64s (sides a, b, c)
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:?} → area = {:.4}", shape, area(shape));
}
}
Output:
Circle(5.0) → area = 78.5398
Rectangle(4.0, 6.0) → area = 24.0000
Triangle(3.0, 4.0, 5.0) → area = 6.0000
Enums with Struct-like Variants
Variants can even have named fields, just like structs:
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn process(msg: Message) {
match msg {
Message::Quit => {
println!("Quit message — shutting down.");
}
Message::Move { x, y } => {
println!("Moving to position ({}, {})", x, y);
}
Message::Write(text) => {
println!("Writing: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("Changing color to RGB({}, {}, {})", r, g, b);
}
}
}
fn main() {
process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("Hello, Rust!")));
process(Message::ChangeColor(255, 128, 0));
process(Message::Quit);
}
Output:
Moving to position (10, 20)
Writing: Hello, Rust!
Changing color to RGB(255, 128, 0)
Quit message — shutting down.
The match Expression
match is Rust's pattern-matching powerhouse. It's like switch in other languages, but far more powerful.
Match Must Be Exhaustive
Unlike switch in C/Java, match must handle every possible case:
fn main() {
let number = 7;
match number {
1 => println!("One"),
2 | 3 => println!("Two or three"), // Multiple patterns with |
4..=6 => println!("Between four and six"), // Range pattern
7 => println!("Seven"),
_ => println!("Something else"), // `_` is the catch-all
}
}
Output:
Seven
Binding Values in Match
fn describe_number(n: i32) -> String {
match n {
0 => String::from("zero"),
1..=9 => format!("{} is a single digit", n),
10..=99 => format!("{} is a double digit", n),
100..=999 => format!("{} is a triple digit", n),
other => format!("{} is a large number", other), // binds the value to `other`
}
}
fn main() {
for n in [0, 5, 42, 100, 9999] {
println!("{}", describe_number(n));
}
}
Output:
zero
5 is a single digit
42 is a double digit
100 is a triple digit
9999 is a large number
Match with Guards
Add conditions to match arms using if:
fn classify(n: i32) {
match n {
x if x < 0 => println!("{} is negative", x),
0 => println!("zero"),
x if x % 2 == 0 => println!("{} is positive and even", x),
x => println!("{} is positive and odd", x),
}
}
fn main() {
classify(-5);
classify(0);
classify(4);
classify(7);
}
Output:
-5 is negative
zero
4 is positive and even
7 is positive and odd
Option<T> — Rust's Answer to Null
Many languages have null or None values that can cause crashes when you forget to check for them. Rust eliminates null entirely with the Option<T> enum:
enum Option<T> {
Some(T), // Contains a value of type T
None, // No value
}
You can't accidentally use an Option<T> as if it were a T — the compiler forces you to handle both cases.
fn find_first_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return Some(n); // Found a value — wrap it in Some
}
}
None // No even number found
}
fn main() {
let odds = vec![1, 3, 5, 7, 9];
let mixed = vec![1, 3, 4, 6, 7];
// Handle Option with match
match find_first_even(&odds) {
Some(n) => println!("First even in odds: {}", n),
None => println!("No even number found in odds"),
}
match find_first_even(&mixed) {
Some(n) => println!("First even in mixed: {}", n),
None => println!("No even number found in mixed"),
}
}
Output:
No even number found in odds
First even in mixed: 4
Useful Option Methods
fn main() {
let some_value: Option<i32> = Some(42);
let no_value: Option<i32> = None;
// unwrap_or: get the value or a default
println!("{}", some_value.unwrap_or(0)); // 42
println!("{}", no_value.unwrap_or(0)); // 0
// map: transform the inner value if Some
let doubled = some_value.map(|v| v * 2);
println!("{:?}", doubled); // Some(84)
// is_some / is_none
println!("some_value is some? {}", some_value.is_some()); // true
println!("no_value is none? {}", no_value.is_none()); // true
// unwrap_or_else: use a closure for the default
let result = no_value.unwrap_or_else(|| {
println!("Computing default...");
99
});
println!("Result: {}", result);
}
Output:
42
0
Some(84)
some_value is some? true
no_value is none? true
Computing default...
Result: 99
if let — Simpler Pattern Matching
When you only care about one pattern, if let is cleaner than a full match:
fn main() {
let favorite_number: Option<u32> = Some(7);
// Instead of:
match favorite_number {
Some(n) => println!("Favorite number is {}", n),
None => {}, // Do nothing
}
// Use if let:
if let Some(n) = favorite_number {
println!("Favorite number is {}", n);
}
// if let with else
let config_value: Option<&str> = None;
if let Some(val) = config_value {
println!("Config: {}", val);
} else {
println!("No config found, using defaults.");
}
}
Output:
Favorite number is 7
Favorite number is 7
No config found, using defaults.
while let — Loop While Pattern Matches
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
// Pop elements until the stack is empty
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
println!("Stack is empty!");
}
Output:
Popped: 5
Popped: 4
Popped: 3
Popped: 2
Popped: 1
Stack is empty!
Putting It All Together: A Mini State Machine
#[derive(Debug)]
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
fn duration_seconds(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
fn can_go(&self) -> bool {
matches!(self, TrafficLight::Green) // `matches!` macro — a shorthand
}
}
fn main() {
let mut light = TrafficLight::Red;
for _ in 0..6 {
println!(
"{:?} — {}s — Go? {}",
light,
light.duration_seconds(),
light.can_go()
);
light = light.next();
}
}
Output:
Red — 60s — Go? false
Green — 45s — Go? true
Yellow — 5s — Go? false
Red — 60s — Go? false
Green — 45s — Go? true
Yellow — 5s — Go? false
Summary
In this post you learned:
- ✅ Enums can hold data in each variant — simple values, tuples, or named fields
- ✅
matchis exhaustive — every case must be handled - ✅ Match guards (
ifconditions) add flexibility - ✅
Option<T>replaces null —Some(value)orNone - ✅
if letsimplifies single-pattern matching - ✅
while letloops while a pattern continues to match
What's Next?
In Post 7, we'll cover Error Handling — one of Rust's most practical strengths. You'll learn about Result<T, E>, the ? operator, and how to write robust programs that handle failure gracefully.
Next Post: Error Handling in Rust →
Part of the Learn Rust from Scratch series on CodeWithNeo


Leave a Reply