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



2. Conceptual Diagrams

100%
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:

TEXT 📖 Display only
0 = Payable, 1 = Paid, 2 = Shipped, 3 = Completed
RUST
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

RUST
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 match with an enum ensures exhaustive checking—every state is handled, with none omitted.



4. Enumeration Types

(1) Enumeration variants can carry data

RUST
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.

100%
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:

TEXT 📖 Display only
Quit: Exit
Move: Move to (<x>, <y>)
Write: Message Content: <text>
ChangeColor: Color RGB(<r>, <g>, <b>)
RUST
// ============================================
// 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:

TEXT 📖 Display only
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. Quit has no data, Move has an anonymous struct, Write has a String, and ChangeColor has three i32s. This is where enums are more flexible than structs.


▶ Example 2: Option<T>—Handling Null Values in Rust (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
10 / 2 = <value>
The divisor is 0
10 / 0 = <value>
The divisor is 0
<result2.unwrap()>
result1 the value of: <result1.unwrap()>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 with null/nil/None, Rust’s Option<T> forces you to explicitly handle both the “non-null” and “null” cases using match or unwrap.


▶ Example 3: Common Methods of Option (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
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()>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 write match every time. map converts values, unwrap_or provides default values, and expect offers clearer error messages during debugging.


▶ Example 4: Exhaustiveness of Enumerations and match (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
Lucky Penny!
Value of Coins: <value_in_cents(coin)> cents
IPv4: <a>.<b>.<c>.<d>
IPv6: <addr>
RUST
// ============================================
// 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:

TEXT 📖 Display only
Value of Coins: 25 cents
IPv4: 127.0.0.1

Every variant of an enum must be handled in a match statement—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’s switch (in C, if you forget a case in a switch statement, it will simply skip it silently).


▶ Example 5: Comprehensive Exercise—User Input Parser (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
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>
RUST
// ============================================
// 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:

TEXT 📖 Display only
=== 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, Option chained operations (ok()?, .ok()?), match exhaustive checks, and the parse_command return Option<Command> pattern. Enum + Option + match is the standard paradigm in Rust for handling "multiple possible outcomes."


❓ FAQ

Q What is the difference between an enumeration and a struct?
A A struct is a "both-and" (and type), while an enumeration is an "either-or" (or type).
Q Why does Rust use Option instead of null?
A Option is type-safe—the compiler forces you to handle cases where a value "might be null."
Q What's the difference between some and unwrap?
A some means "there might be a value," while unwrap means "I'm sure there's a value; otherwise, it'll crash."
Q How much data can an enumeration store?
A The space occupied by an enumeration = the size of the largest variant + a 1-byte tag.
Q How do you name the data in enum variants?
A Use anonymous struct syntax.

📖 Summary


📝 Exercises

  1. Difficulty ⭐: Define an enumeration Weekday containing Monday through Sunday. Write a function that takes Weekday as input and returns whether it is a weekday or a weekend (using match).
  2. Difficulty ⭐⭐: Write a function fn safe_sqrt(x: f64) -> Option<f64> that returns Some(x.sqrt()) if x >= 0, and None otherwise. Test both positive and negative numbers in main.
  3. Difficulty ⭐⭐⭐: Define an enumeration Temperature that includes two variants: Celsius(f64) and Fahrenheit(f64). Implement methods fn to_celsius(&self) -> f64 and fn to_fahrenheit(&self) -> f64. In main, create two temperature values and convert between them.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏