Rust: Rust Enums and Option
Last updated: 2026-08-26
Enums are one of Rust’s most powerful types—they allow a value to be one of a set of possible variants, and each variant can carry different data.
If a struct is an "and" type (it has all fields at the same time), then an enum is an "or" type (it's either this or that).
1. What You'll Learn
- Defining Enumeration Types and Variants
- Enumeration variants can carry different types of data
- Use
implto add methods to an enumeration Option<T>Enumeration—Rust's Approach to "Null Values"- Checking the Exhaustiveness of Enumerations and
matchStatements Option<T>common methods (unwrap, map, expect)
2. Conceptual Diagrams
flowchart LR
subgraph "enum Option<T>"
S["Some(T)<br>Not null"]
N["None<br>No value"]
end
subgraph "Use Cases"
V["Calculation Results"] -->|"Success"| S
F["Calculation Results"] -->|"Failure/None"| N
end
S --> MATCH["match Processing"]
N --> MATCH
MATCH -->|"Some(v) => Usage Value v"| OK["✅ Safety"]
MATCH -->|"None => Handling Null Values"| SAFE["✅ No crashes"]
3. The Story of an Order System
(1) Pain Point: Representing Order Status with Numbers
Tom is developing an e-commerce system. Orders have four statuses:
0 = Payable, 1 = Paid, 2 = Shipped, 3 = Completed
let order_status = 0; // Payable
// But what if someone were to write order_status = 99?
// Or confuse the status with the quantity: let order_status = product_count;
Representing states with integers is a common practice in C—it’s not type-safe. Any integer can be assigned to a “state,” and the compiler won’t check it for you. Furthermore, there’s no relationship between states, which makes it prone to errors.
(2) Solutions for Rust Enumerations
enum OrderStatus {
Pending, // Payable
Paid, // Paid
Shipped, // Shipped -- you can bring your shipping tracking number
Delivered, // Completed -- delivery time (in-person)
}
fn main() {
let status = OrderStatus::Paid;
match status {
OrderStatus::Pending => println!("Please complete the payment"),
OrderStatus::Paid => println!("Paid, awaiting shipment"),
OrderStatus::Shipped => println!("Shipped, on the way"),
OrderStatus::Delivered => println!("Delivered, thank you for your purchase"),
}
// The compiler ensures that: you won't forget to handle any state!
// If a new status is added Cancelled, forgot to update match, the compiler will report an error
}
An enum groups all possible "options" into a single type, and using
matchwith an enum ensures exhaustive checking—every state is handled, with none omitted.
4. Enumeration Types
(1) Enumeration variants can carry data
enum Message {
Quit, // No data available
Move { x: i32, y: i32 }, // Anonymous Structures
Write(String), // Single value
ChangeColor(i32, i32, i32), // Tuple
}
| Enumeration Variant | Data Carried | Usage |
|---|---|---|
Quit |
None | Just a marker |
Move { x, y } |
Anonymous structure | Message::Move { x: 10, y: 20 } |
Write(String) |
Tuple Structure | Message::Write("hello".to_string()) |
ChangeColor(i,i,i) |
Tuple Structure | Message::ChangeColor(255, 0, 0) |
(2) Quick Reference for Common Option Methods
| Method | Return Type | Description | Behavior when None |
|---|---|---|---|
unwrap() |
T |
Extract Value | panic |
expect(msg) |
T |
Retrieve value (custom message) | panic + msg |
unwrap_or(default) |
T |
Retrieve value or default value | Return default |
map(f) |
Option<U> |
Convert the value in "Some" | Keep "None" |
and_then(f) |
Option<U> |
Chain Option Operation | Keep None |
filter(f) |
Option<T> |
Condition Filter | None if not met |
is_some() |
bool |
Has a value | false |
is_none() |
bool |
Is None | true |
ok_or(err) |
Result<T, E> |
Convert to Result | Err(err) |
(3) Comparison of Option and null
| Dimension | Option<T> |
null (Other Languages) |
|---|---|---|
| Type Safety | The compiler enforces handling of null values | Any reference may be null |
| Null checks | Automatic overriding via match/method chaining |
Requires manual if checks |
| Forgot to check | Compilation error | Runtime NullPointerException |
| Chained operations | map/and_then, etc. | Requires nested if statements or optional chaining |
(4) Memory Layout of Enums
Memory used by an enumeration = memory used by the largest variant + 1 byte for the tag. The compiler uses the tag to determine which variant is currently stored.
graph TB
subgraph "Message Enumerations in Memory"
TAG[tag: 1 Byte] -->|Indicates which variant is currently selected| LABEL
DATA[Data on the Largest Variant: 8 Byte] --> LABEL[In total 9 Byte]
end
5. Enumeration Examples
▶ Example 1: Enum variants carry different types of data (Difficulty ⭐⭐)
Output:
Quit: Exit
Move: Move to (<x>, <y>)
Write: Message Content: <text>
ChangeColor: Color RGB(<r>, <g>, <b>)
// ============================================
// Enumeration variants carry data -- Message Type
// ============================================
#[derive(Debug)]
enum Message {
Quit, // No data available
Move { x: i32, y: i32 }, // Anonymous Structures
Write(String), // Tuple Structure
ChangeColor(i32, i32, i32), // Tuple Structure
}
impl Message {
fn call(&self) {
match self {
Message::Quit => println!("Quit: Exit"),
Message::Move { x, y } => println!("Move: Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: Message Content: {}", text),
Message::ChangeColor(r, g, b) => {
println!("ChangeColor: Color RGB({}, {}, {})", r, g, b);
}
}
}
}
fn main() {
let messages = vec![
Message::Write(String::from("Hello")),
Message::Move { x: 10, y: 20 },
Message::ChangeColor(255, 0, 0),
Message::Quit,
];
for msg in &messages {
msg.call();
}
}
Output:
Write: Message Content: Hello
Move: Move to (10, 20)
ChangeColor: Color RGB(255, 0, 0)
Quit: Exit
Each variant of an enum can carry a different number and type of data.
Quithas no data,Movehas an anonymous struct,Writehas a String, andChangeColorhas three i32s. This is where enums are more flexible than structs.
▶ Example 2: Option<T>—Handling Null Values in Rust (Difficulty ⭐⭐)
Output:
10 / 2 = <value>
The divisor is 0
10 / 0 = <value>
The divisor is 0
<result2.unwrap()>
result1 the value of: <result1.unwrap()>
// ============================================
// Option<T> Enumeration: Not null (Some) or null (None)
// ============================================
// Option Definition (in the standard library)
// enum Option<T> {
// Some(T),
// None,
// }
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // Back "no value" -- instead of crashing or returning NaN
} else {
Some(a / b) // Back "not null"
}
}
fn main() {
let result1 = divide(10.0, 2.0);
let result2 = divide(10.0, 0.0);
// Use match to process Option
match result1 {
Some(value) => println!("10 / 2 = {}", value),
None => println!("The divisor is 0"),
}
match result2 {
Some(value) => println!("10 / 0 = {}", value),
None => println!("The divisor is 0"),
}
// Simplify: unwrap or expect (risky, but convenient)
// println!("{}", result2.unwrap()); // ❌ unwrap on None will panic!
println!("result1 the value of: {}", result1.unwrap()); // ✅ unwrap on Some is safe
}
Output:
10 / 2 = 5
The divisor is 0
result1 the value of: 5
Option<T>is Rust's standard way of handling "may be null" values. Unlike other languages that cause a runtime crash withnull/nil/None, Rust’sOption<T>forces you to explicitly handle both the “non-null” and “null” cases usingmatchorunwrap.
▶ Example 3: Common Methods of Option (Difficulty ⭐⭐)
Output:
map after: <doubled_some>, <doubled_none>
unwrap_or: <some_value.unwrap_or(0)>, <// 10
none_value.unwrap_or(0)>
expect: <some_value.expect("There should be a value")>
expect: <none_value.expect("OH NO! No value!")>
is_some: <some_value.is_some()>, is_none: <none_value.is_none()>
// ============================================
// Option Practical Methods for: map, unwrap_or, expect
// ============================================
fn main() {
let some_value: Option<i32> = Some(10);
let none_value: Option<i32> = None;
// map: If there is a value, convert it; no value, maintain None
let doubled_some = some_value.map(|x| x * 2);
let doubled_none = none_value.map(|x| x * 2);
println!("map after: {:?}, {:?}", doubled_some, doubled_none);
// unwrap_or: Returns a value, if no value is provided, the default is used.
println!("unwrap_or: {}, {}",
some_value.unwrap_or(0), // 10
none_value.unwrap_or(0), // 0
);
// expect: Returns a value, if no value, panic and display a custom message
println!("expect: {}", some_value.expect("There should be a value"));
// println!("expect: {}", none_value.expect("OH NO! No value!")); // ❌ panic
// is_some / is_none: Check if a value exists
println!("is_some: {}, is_none: {}",
some_value.is_some(),
none_value.is_none(),
);
// Example of a Chain Call
let result = Some(5)
.map(|x| x + 3)
.map(|x| x * 2)
.unwrap_or(0);
println!("Results of a chained call: {}", result); // 16
}
Output:
map after: Some(20), None
unwrap_or: 10, 0
expect: 10
is_some: true, is_none: true
Results of a chained call: 16
Option<T>provides a set of methods that allow you to chain operations on potentially null values in a functional style, without having to writematchevery time.mapconverts values,unwrap_orprovides default values, andexpectoffers clearer error messages during debugging.
▶ Example 4: Exhaustiveness of Enumerations and match (Difficulty ⭐⭐⭐)
Output:
Lucky Penny!
Value of Coins: <value_in_cents(coin)> cents
IPv4: <a>.<b>.<c>.<d>
IPv6: <addr>
// ============================================
// Enumerate proprietary match Exhaustive Check Demonstration
// ============================================
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky Penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
// If I don't write Quarter, the compiler will report an error: non-exhaustive patterns
}
}
// Enumeration with Data
#[derive(Debug)]
enum IpAddr {
V4(u8, u8, u8, u8), // 4 bytes
V6(String), // Complete IPv6 Address
}
fn main() {
let coin = Coin::Quarter;
println!("Value of Coins: {} cents", value_in_cents(coin));
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
match home {
IpAddr::V4(a, b, c, d) => {
println!("IPv4: {}.{}.{}.{}", a, b, c, d);
}
IpAddr::V6(addr) => {
println!("IPv6: {}", addr);
}
}
}
Output:
Value of Coins: 25 cents
IPv4: 127.0.0.1
Every variant of an enum must be handled in a
matchstatement—this is known as "exhaustive checking." If you forget to handle a variant, the compiler will report an error. This is much safer than C’sswitch(in C, if you forget acasein aswitchstatement, it will simply skip it silently).
▶ Example 5: Comprehensive Exercise—User Input Parser (Difficulty ⭐⭐⭐)
Output:
Command: help | greet <name> | calc <a> <op> <b> | quit
Hello, <name>!
Error: Divisor is zero
Unknown Operator: <op>
<op> <b> <r> = <a>
Goodbye!
=== Command Parser ===
> <input>
// ============================================
// Comprehensive Example: Enumeration + Option + match Real-World Experience
// ============================================
#[derive(Debug)]
enum Command {
Help,
Greet(String),
Calc(f64, char, f64),
Quit,
}
fn parse_command(input: &str) -> Option<Command> {
let parts: Vec<&str> = input.trim().split_whitespace().collect();
if parts.is_empty() {
return None;
}
match parts[0] {
"help" | "h" => Some(Command::Help),
"quit" | "q" => Some(Command::Quit),
"greet" if parts.len() > 1 => {
Some(Command::Greet(parts[1..].join(" ")))
}
"calc" if parts.len() == 4 => {
let a = parts[1].parse::<f64>().ok()?;
let op = parts[2].chars().next()?;
let b = parts[3].parse::<f64>().ok()?;
Some(Command::Calc(a, op, b))
}
_ => None,
}
}
fn execute(cmd: Command) -> bool {
match cmd {
Command::Help => {
println!("Command: help | greet <name> | calc <a> <op> <b> | quit");
true
}
Command::Greet(name) => {
println!("Hello, {}!", name);
true
}
Command::Calc(a, op, b) => {
let result = match op {
'+' => Some(a + b),
'-' => Some(a - b),
'*' => Some(a * b),
'/' if b != 0.0 => Some(a / b),
'/' => { println!("Error: Divisor is zero"); None }
_ => { println!("Unknown Operator: {}", op); None }
};
result.map(|r| println!("{} {} {} = {:.2}", a, op, b, r));
true
}
Command::Quit => {
println!("Goodbye!");
false
}
}
}
fn main() {
let inputs = [
"help",
"greet Alice",
"calc 10 + 5",
"calc 20 / 4",
"calc 1 / 0",
"unknown",
"quit",
];
println!("=== Command Parser ===");
for input in inputs {
println!("\n> {}", input);
match parse_command(input) {
Some(cmd) => {
if !execute(cmd) { break; }
}
None => println!("Unrecognized command"),
}
}
}
Output:
=== Command Parser ===
> help
Command: help | greet <name> | calc <a> <op> <b> | quit
> greet Alice
Hello, Alice!
> calc 10 + 5
10 + 5 = 15.00
> calc 20 / 4
20 / 4 = 5.00
> calc 1 / 0
Error: Divisor is zero
> unknown
Unrecognized command
> quit
Goodbye!
This example combines the use of enum variants carrying different data,
Optionchained operations (ok()?,.ok()?),matchexhaustive checks, and theparse_commandreturnOption<Command>pattern. Enum + Option + match is the standard paradigm in Rust for handling "multiple possible outcomes."
❓ FAQ
Option instead of null?Option is type-safe—the compiler forces you to handle cases where a value "might be null."some and unwrap?some means "there might be a value," while unwrap means "I'm sure there's a value; otherwise, it'll crash."📖 Summary
- Enum is a "union type"—a value can be one of several variants
- Enum variants can carry data (in the form of tuples or anonymous structs)
Option<T>is the most commonly used enumeration in the standard library, serving as a substitute for null- When used with
match, the compiler enforces exhaustive checkingOptionprovides chained methods such asmap,unwrap_or, andexpect. - Memory usage of an enumeration ≈ maximum variant size + 1 byte tag
📝 Exercises
- Difficulty ⭐: Define an enumeration
Weekdaycontaining Monday through Sunday. Write a function that takesWeekdayas input and returns whether it is a weekday or a weekend (usingmatch). - Difficulty ⭐⭐: Write a function
fn safe_sqrt(x: f64) -> Option<f64>that returnsSome(x.sqrt())if x >= 0, andNoneotherwise. Test both positive and negative numbers inmain. - Difficulty ⭐⭐⭐: Define an enumeration
Temperaturethat includes two variants:Celsius(f64)andFahrenheit(f64). Implement methodsfn to_celsius(&self) -> f64andfn to_fahrenheit(&self) -> f64. Inmain, create two temperature values and convert between them.